[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-overscroll-behavior-scroll-containment":44,"search-suggestions":245,"related-overscroll-behavior-scroll-containment":283,"code:css:true:1tvdj6u":417,"code:js:true:cpl3bq":418,"code:css:true:1jaz749":419,"code:css:true:o46ebg":420,"code:css:true:1k77v0i":421,"code:css:true:kdch3d":422,"comments-019ff19b-f763-70e0-abdf-eded59c8e69a":423},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":75,"playground":77,"body":79,"bodyMd":227,"seo":228,"translationGroupId":232,"series":48,"podcastUrl":48,"verticalId":5,"thread":233,"assessments":235,"translations":241,"quiz":243},"019ff19b-f763-70e0-abdf-eded59c8e69a","overscroll-behavior-scroll-containment","You're blocking touchmove events to contain scroll. `overscroll-behavior` does it natively.",null,"When a scrollable container hits its edge, the page behind it starts scrolling too. Developers block this with JavaScript event handlers or body overflow tricks. overscroll-behavior is the CSS property that turns off scroll chaining in one line.","\u002Fmedia\u002Fcovers\u002Foverscroll-behavior-scroll-containment.png",4,"2026-08-12T07:38:53.786Z",636,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"css","CSS",[64,66,69,72],{"slug":61,"name":65,"color":48},"Css",{"slug":67,"name":68,"color":48},"webdev","Webdev",{"slug":70,"name":71,"color":48},"frontend","Frontend",{"slug":73,"name":74,"color":48},"javascript","Javascript",{"assessments":76},1,{"slug":46,"title":78},"overscroll-behavior — interactive playground",{"blocks":80,"version":76},[81,85,89,92,99,104,107,110,113,118,121,124,128,131,134,137,140,144,147,150,154,157,160,163,167,170,173,176,179,182,185,188,191,194,197,200,203,206,209,212,215,218,221],{"id":82,"html":83,"type":84},"b1","\u003Cp>When a user scrolls to the bottom of a modal and keeps scrolling, the page behind it starts moving. This is \u003Cstrong>scroll chaining\u003C\u002Fstrong> — the browser passes the scroll gesture up through the DOM when the focused container runs out of room. The traditional fix is JavaScript: intercept \u003Ccode>touchmove\u003C\u002Fcode> events, check whether the container has hit its boundary, and call \u003Ccode>preventDefault()\u003C\u002Fcode>. There&#39;s a CSS property for this.\u003C\u002Fp>","paragraph",{"id":86,"html":87,"text":87,"type":88,"level":31},"b2","The property","heading",{"id":90,"html":91,"type":84},"b3","\u003Cp>\u003Ccode>overscroll-behavior\u003C\u002Fcode> controls what happens when a scroll gesture reaches the edge of a scrollable container. Three values:\u003C\u002Fp>",{"id":93,"type":94,"items":95,"ordered":18},"b4","list",[96,97,98],"\u003Ccode>auto\u003C\u002Fcode> — the default; scroll chains to the parent element","\u003Ccode>contain\u003C\u002Fcode> — stops chaining at this element; overscroll effects (bounce, glow highlight) still render","\u003Ccode>none\u003C\u002Fcode> — stops chaining \u003Cem>and\u003C\u002Fem> suppresses the overscroll effect entirely",{"id":100,"code":101,"type":102,"language":61,"highlight":103},"b5",".modal-body {\n  overflow-y: auto;\n  overscroll-behavior-y: contain;\n}","code",[],{"id":105,"html":106,"type":84},"b6","\u003Cp>One property on one element. The browser&#39;s own scroll machinery handles containment — no event listener, no JavaScript.\u003C\u002Fp>",{"id":108,"html":109,"text":109,"type":88,"level":31},"b7","What it replaces",{"id":111,"html":112,"type":84},"b8","\u003Cp>Here&#39;s the JavaScript approach, written carefully enough to actually work:\u003C\u002Fp>",{"id":114,"code":115,"type":102,"language":116,"highlight":117},"b9","\u002F\u002F Before — fragile, performance-hostile\nlet touchStartY = 0;\n\nmodal.addEventListener('touchstart', (e) => {\n  touchStartY = e.touches[0].clientY;\n}, { passive: true });\n\nmodal.addEventListener('touchmove', (e) => {\n  const delta = touchStartY - e.touches[0].clientY;\n  const atBottom = modal.scrollTop >= modal.scrollHeight - modal.clientHeight;\n  const atTop    = modal.scrollTop \u003C= 0;\n\n  if ((atBottom && delta > 0) || (atTop && delta \u003C 0)) {\n    e.preventDefault(); \u002F\u002F ← requires passive: false\n  }\n}, { passive: false }); \u002F\u002F ⚠️ kills scroll performance","js",[],{"id":119,"html":120,"type":84},"b10","\u003Cp>\u003Ccode>passive: false\u003C\u002Fcode> is the problem. Browsers optimistically start compositing the scroll on the GPU — when a listener is passive, they don&#39;t wait for it. The moment you declare \u003Ccode>passive: false\u003C\u002Fcode>, the browser has to park the entire scroll gesture, wait for your handler to finish, then decide whether to scroll. You pay that cost on every \u003Ccode>touchmove\u003C\u002Fcode> event, on every device, whether the user is anywhere near the edge or not. Chrome logs a warning about this in DevTools.\u003C\u002Fp>",{"id":122,"html":123,"type":84},"b11","\u003Cp>With CSS:\u003C\u002Fp>",{"id":125,"code":126,"type":102,"language":61,"highlight":127},"b12","\u002F* After — one declaration, no event listener *\u002F\n.modal-body {\n  overflow-y: auto;\n  overscroll-behavior-y: contain;\n}",[],{"id":129,"html":130,"type":84},"b13","\u003Cp>No passive penalty. No edge-case bugs where the user simultaneously hits the top and bottom boundary during a fast flick. No listener to clean up when the modal unmounts.\u003C\u002Fp>",{"id":132,"html":133,"text":133,"type":88,"level":31},"b14","Pull-to-refresh and browser navigation",{"id":135,"html":136,"type":84},"b15","\u003Cp>\u003Ccode>overscroll-behavior\u003C\u002Fcode> controls two other native gestures besides scroll chaining.\u003C\u002Fp>",{"id":138,"html":139,"type":84},"b16","\u003Cp>\u003Cstrong>Pull-to-refresh on mobile Chrome.\u003C\u002Fstrong> Overscrolling past the top of the page triggers the refresh gesture. If you&#39;ve built a custom refresh indicator, or you&#39;re shipping an app shell where the native gesture doesn&#39;t belong, disable it on the document:\u003C\u002Fp>",{"id":141,"code":142,"type":102,"language":61,"highlight":143},"b17","body {\n  overscroll-behavior-y: none;\n}",[],{"id":145,"html":146,"type":84},"b18","\u003Cp>Use this deliberately. Pull-to-refresh is a familiar gesture — suppressing it at the \u003Ccode>body\u003C\u002Fcode> level surprises users on pages that function as documents. Reserve it for true app shells.\u003C\u002Fp>",{"id":148,"html":149,"type":84},"b19","\u003Cp>\u003Cstrong>Back-navigation swipes on horizontal carousels.\u003C\u002Fstrong> On macOS trackpads and some mobile browsers, swiping past the last card in a horizontal scroll container triggers browser back\u002Fforward navigation. This is almost never what you want:\u003C\u002Fp>",{"id":151,"code":152,"type":102,"language":61,"highlight":153},"b20",".carousel {\n  overflow-x: auto;\n  overscroll-behavior-x: contain;\n}",[],{"id":155,"html":156,"type":84},"b21","\u003Cp>The carousel scrolls freely within its bounds; going past the last slide does nothing instead of navigating away.\u003C\u002Fp>",{"id":158,"html":159,"text":159,"type":88,"level":31},"b22","The shorthand and axis properties",{"id":161,"html":162,"type":84},"b23","\u003Cp>There are two axis-specific variants and a shorthand:\u003C\u002Fp>",{"id":164,"code":165,"type":102,"language":61,"highlight":166},"b24","\u002F* Both axes *\u002F\noverscroll-behavior: contain;\n\n\u002F* Axis-specific *\u002F\noverscroll-behavior-x: contain;\noverscroll-behavior-y: none;\n\n\u002F* Shorthand: x-value then y-value *\u002F\noverscroll-behavior: contain none;",[],{"id":168,"html":169,"type":84},"b25","\u003Cp>The most common pattern is \u003Ccode>overscroll-behavior-y: contain\u003C\u002Fcode> on vertically scrollable panels — modals, drawers, sidebars, dropdown bodies — where you want scroll to stay inside the component without disabling anything horizontally.\u003C\u002Fp>",{"id":171,"html":172,"text":172,"type":88,"level":31},"b26","Browser support",{"id":174,"html":175,"type":84},"b27","\u003Cp>\u003Ccode>overscroll-behavior\u003C\u002Fcode> is \u003Cstrong>Baseline 2019\u003C\u002Fstrong>: Chrome 63 (December 2017), Firefox 59 (March 2018), Safari 16 (September 2022). iOS Safari was the last major holdout, shipping support in late 2022. It&#39;s been broadly available for several years across browsers, Node.js-powered WebViews, and PWA contexts.\u003C\u002Fp>",{"id":177,"html":178,"type":84},"b28","\u003C!-- playground:start -->",{"id":180,"html":181,"text":181,"type":88,"level":31},"b29","🎮 Try it yourself",{"id":183,"html":184,"type":84},"b30","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Foverscroll-behavior-scroll-containment\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":186,"html":187,"type":84},"b31","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":189,"html":190,"type":84},"b32","\u003C!-- playground:end -->",{"id":192,"html":193,"type":84},"b33","\u003C!-- quiz:start -->",{"id":195,"html":196,"text":196,"type":88,"level":31},"b34","🧠 Test yourself",{"id":198,"html":199,"type":84},"b35","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Foverscroll-behavior-scroll-containment\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":201,"html":202,"type":84},"b36","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":204,"html":205,"type":84},"b37","\u003C!-- quiz:end -->",{"id":207,"html":208,"text":208,"type":88,"level":31},"b38","The takeaway",{"id":210,"html":211,"type":84},"b39","\u003Cp>Search your codebase for \u003Ccode>touchmove\u003C\u002Fcode> listeners that call \u003Ccode>e.preventDefault()\u003C\u002Fcode> to stop scroll from escaping a container — those are scroll-chaining patches. Replace the listener with \u003Ccode>overscroll-behavior: contain\u003C\u002Fcode> on the scrollable element and delete the JavaScript. While you&#39;re at it, add \u003Ccode>overscroll-behavior-x: contain\u003C\u002Fcode> to any horizontal scroll container where you don&#39;t want browser navigation triggered at the edges. The CSS path doesn&#39;t touch the browser&#39;s scroll machinery, scales to every device with no tuning, and removes a passive-false listener that was slowing down every scroll gesture regardless of whether it hit the boundary.\u003C\u002Fp>",{"id":213,"type":214},"b40","divider",{"id":216,"html":217,"type":84},"b41","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":219,"html":220,"type":84},"b42","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":222,"type":94,"items":223,"ordered":18},"b43",[224,225,226],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","When a user scrolls to the bottom of a modal and keeps scrolling, the page behind it starts moving. This is **scroll chaining** — the browser passes the scroll gesture up through the DOM when the focused container runs out of room. The traditional fix is JavaScript: intercept `touchmove` events, check whether the container has hit its boundary, and call `preventDefault()`. There's a CSS property for this.\n\n## The property\n\n`overscroll-behavior` controls what happens when a scroll gesture reaches the edge of a scrollable container. Three values:\n\n- `auto` — the default; scroll chains to the parent element\n- `contain` — stops chaining at this element; overscroll effects (bounce, glow highlight) still render\n- `none` — stops chaining *and* suppresses the overscroll effect entirely\n\n```css\n.modal-body {\n  overflow-y: auto;\n  overscroll-behavior-y: contain;\n}\n```\n\nOne property on one element. The browser's own scroll machinery handles containment — no event listener, no JavaScript.\n\n## What it replaces\n\nHere's the JavaScript approach, written carefully enough to actually work:\n\n```js\n\u002F\u002F Before — fragile, performance-hostile\nlet touchStartY = 0;\n\nmodal.addEventListener('touchstart', (e) => {\n  touchStartY = e.touches[0].clientY;\n}, { passive: true });\n\nmodal.addEventListener('touchmove', (e) => {\n  const delta = touchStartY - e.touches[0].clientY;\n  const atBottom = modal.scrollTop >= modal.scrollHeight - modal.clientHeight;\n  const atTop    = modal.scrollTop \u003C= 0;\n\n  if ((atBottom && delta > 0) || (atTop && delta \u003C 0)) {\n    e.preventDefault(); \u002F\u002F ← requires passive: false\n  }\n}, { passive: false }); \u002F\u002F ⚠️ kills scroll performance\n```\n\n`passive: false` is the problem. Browsers optimistically start compositing the scroll on the GPU — when a listener is passive, they don't wait for it. The moment you declare `passive: false`, the browser has to park the entire scroll gesture, wait for your handler to finish, then decide whether to scroll. You pay that cost on every `touchmove` event, on every device, whether the user is anywhere near the edge or not. Chrome logs a warning about this in DevTools.\n\nWith CSS:\n\n```css\n\u002F* After — one declaration, no event listener *\u002F\n.modal-body {\n  overflow-y: auto;\n  overscroll-behavior-y: contain;\n}\n```\n\nNo passive penalty. No edge-case bugs where the user simultaneously hits the top and bottom boundary during a fast flick. No listener to clean up when the modal unmounts.\n\n## Pull-to-refresh and browser navigation\n\n`overscroll-behavior` controls two other native gestures besides scroll chaining.\n\n**Pull-to-refresh on mobile Chrome.** Overscrolling past the top of the page triggers the refresh gesture. If you've built a custom refresh indicator, or you're shipping an app shell where the native gesture doesn't belong, disable it on the document:\n\n```css\nbody {\n  overscroll-behavior-y: none;\n}\n```\n\nUse this deliberately. Pull-to-refresh is a familiar gesture — suppressing it at the `body` level surprises users on pages that function as documents. Reserve it for true app shells.\n\n**Back-navigation swipes on horizontal carousels.** On macOS trackpads and some mobile browsers, swiping past the last card in a horizontal scroll container triggers browser back\u002Fforward navigation. This is almost never what you want:\n\n```css\n.carousel {\n  overflow-x: auto;\n  overscroll-behavior-x: contain;\n}\n```\n\nThe carousel scrolls freely within its bounds; going past the last slide does nothing instead of navigating away.\n\n## The shorthand and axis properties\n\nThere are two axis-specific variants and a shorthand:\n\n```css\n\u002F* Both axes *\u002F\noverscroll-behavior: contain;\n\n\u002F* Axis-specific *\u002F\noverscroll-behavior-x: contain;\noverscroll-behavior-y: none;\n\n\u002F* Shorthand: x-value then y-value *\u002F\noverscroll-behavior: contain none;\n```\n\nThe most common pattern is `overscroll-behavior-y: contain` on vertically scrollable panels — modals, drawers, sidebars, dropdown bodies — where you want scroll to stay inside the component without disabling anything horizontally.\n\n## Browser support\n\n`overscroll-behavior` is **Baseline 2019**: Chrome 63 (December 2017), Firefox 59 (March 2018), Safari 16 (September 2022). iOS Safari was the last major holdout, shipping support in late 2022. It's been broadly available for several years across browsers, Node.js-powered WebViews, and PWA contexts.\n\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Foverscroll-behavior-scroll-containment\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Foverscroll-behavior-scroll-containment\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n## The takeaway\n\nSearch your codebase for `touchmove` listeners that call `e.preventDefault()` to stop scroll from escaping a container — those are scroll-chaining patches. Replace the listener with `overscroll-behavior: contain` on the scrollable element and delete the JavaScript. While you're at it, add `overscroll-behavior-x: contain` to any horizontal scroll container where you don't want browser navigation triggered at the edges. The CSS path doesn't touch the browser's scroll machinery, scales to every device with no tuning, and removes a passive-false listener that was slowing down every scroll gesture regardless of whether it hit the boundary.\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":229,"canonical":230,"description":231},"You're blocking touchmove events to contain scroll. `overscroll-behavi","https:\u002F\u002Fbestpractic.org\u002Fblog\u002Foverscroll-behavior-scroll-containment","When a scrollable container hits its edge, the page behind it starts scrolling too. Developers block this with JavaScript event handlers or body overflow tricks. overscroll-behavio","019ff19b-f763-70e0-abdf-f2a5c248d9b0",{"id":234,"locked":18},"019ff19b-f77c-7582-b325-fcfc22f4d944",[236],{"id":237,"slug":46,"title":238,"_count":239},"019ff19b-f793-7669-8647-56d2a18e90b9","overscroll-behavior — scroll containment",{"questions":240},7,[242],{"locale":13,"slug":46},{"id":237,"slug":46,"title":238,"_count":244,"questionCount":240},{"questions":240},[246,248,250,252,254,258,262,266,270,274,277,280],{"slug":67,"name":68,"articles":247},107,{"slug":73,"name":74,"articles":249},89,{"slug":70,"name":71,"articles":251},72,{"slug":61,"name":65,"articles":253},36,{"slug":255,"name":256,"articles":257},"tutorial","Tutorial",35,{"slug":259,"name":260,"articles":261},"typescript","Typescript",15,{"slug":263,"name":264,"articles":265},"react","React",13,{"slug":267,"name":268,"articles":269},"performance","Performance",12,{"slug":271,"name":272,"articles":273},"browser","Browser",9,{"slug":275,"name":276,"articles":273},"node","Node",{"slug":278,"name":279,"articles":240},"html","Html",{"slug":281,"name":282,"articles":240},"accessibility","Accessibility",{"items":284,"meta":416},[285,303,322,340,359,377,398],{"id":286,"slug":287,"title":288,"subtitle":48,"excerpt":289,"coverUrl":290,"locale":13,"readingMinutes":51,"publishedAt":291,"viewCount":292,"likeCount":19,"commentCount":19,"author":293,"vertical":294,"topic":295,"tags":296,"_count":301,"playground":302,"hasQuiz":17,"hasPlayground":17},"01a0a9ea-93a3-7734-b359-6d7cc4df502b","customizable-select-appearance-base-select","The Dropdown You've Been Faking for a Decade","Every hand-built 'custom select' with icons and a styled popup exists because the real \u003Cselect> couldn't do that. appearance: base-select just closed the gap — without you losing the native picker's keyboard nav and mobile UI.","\u002Fmedia\u002Fcovers\u002Fcustomizable-select-appearance-base-select.png","2026-09-22T19:36:39.450Z",24,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[297,298,299,300],{"slug":61,"name":65,"color":48},{"slug":278,"name":279,"color":48},{"slug":67,"name":68,"color":48},{"slug":281,"name":282,"color":48},{"assessments":76},{"slug":287},{"id":304,"slug":305,"title":306,"subtitle":48,"excerpt":307,"coverUrl":308,"locale":13,"readingMinutes":309,"publishedAt":310,"viewCount":311,"likeCount":19,"commentCount":19,"author":312,"vertical":313,"topic":314,"tags":315,"_count":320,"playground":321,"hasQuiz":17,"hasPlayground":17},"01a0af18-24b4-73ad-95ed-c9479ae0e91f","css-dynamic-viewport-units-dvh-svh-lvh","Stop Writing JavaScript To Fix `100vh` On Mobile","Your full-screen hero looks perfect in the simulator and cuts off the CTA on a real phone. It's not a bug in your code — it's `vh` answering a question you didn't ask. Three new units fix it with zero JavaScript.","\u002Fmedia\u002Fcovers\u002Fcss-dynamic-viewport-units-dvh-svh-lvh.png",6,"2026-09-22T19:35:37.351Z",25,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[316,317,318,319],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":255,"name":256,"color":48},{"assessments":76},{"slug":305},{"id":323,"slug":324,"title":325,"subtitle":48,"excerpt":326,"coverUrl":327,"locale":13,"readingMinutes":51,"publishedAt":328,"viewCount":329,"likeCount":19,"commentCount":19,"author":330,"vertical":331,"topic":332,"tags":333,"_count":338,"playground":339,"hasQuiz":17,"hasPlayground":17},"01a09a84-4147-713b-b0e9-9ffc501e378b","css-color-scheme-native-dark-mode-form-controls","The CSS Property That Fixes Dark Mode's Ugly White Boxes","You shipped dark mode. The page is black, the text is white — and then the date picker opens like a floodlight. color-scheme is the one-line CSS property that tells the browser your native form controls need to catch up too.","\u002Fmedia\u002Fcovers\u002Fcss-color-scheme-native-dark-mode-form-controls.png","2026-09-18T17:22:32.837Z",145,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[334,335,336,337],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":281,"name":282,"color":48},{"assessments":76},{"slug":324},{"id":341,"slug":342,"title":343,"subtitle":48,"excerpt":344,"coverUrl":345,"locale":13,"readingMinutes":309,"publishedAt":346,"viewCount":347,"likeCount":19,"commentCount":19,"author":348,"vertical":349,"topic":350,"tags":352,"_count":357,"playground":358,"hasQuiz":17,"hasPlayground":17},"01a09525-10c5-714d-9021-2e375a15a5d3","matchmedia-watch-media-queries-from-js","Stop Debouncing Resize. The Browser Already Watches It.","A debounced resize handler comparing window.innerWidth to a magic number can't ever see dark mode or reduced motion — and it fires dozens of times for one thing matchMedia fires once for.","\u002Fmedia\u002Fcovers\u002Fmatchmedia-watch-media-queries-from-js.png","2026-09-17T11:19:53.454Z",328,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":73,"name":351},"JavaScript",[353,354,355,356],{"slug":73,"name":74,"color":48},{"slug":67,"name":68,"color":48},{"slug":61,"name":65,"color":48},{"slug":271,"name":272,"color":48},{"assessments":76},{"slug":342},{"id":360,"slug":361,"title":362,"subtitle":48,"excerpt":363,"coverUrl":364,"locale":13,"readingMinutes":309,"publishedAt":365,"viewCount":366,"likeCount":19,"commentCount":19,"author":367,"vertical":368,"topic":369,"tags":370,"_count":375,"playground":376,"hasQuiz":17,"hasPlayground":17},"01a080a9-801c-727a-ad92-866c15063710","css-custom-highlight-api-no-dom-mutation","The Search Highlight That Deletes Your Selection","A search-highlight feature that rebuilds its DOM on every keystroke can wipe out a user's own text selection with no warning. The CSS Custom Highlight API fixes it by styling text ranges without ever touching a DOM node.","\u002Fmedia\u002Fcovers\u002Fcss-custom-highlight-api-no-dom-mutation.png","2026-09-13T11:25:57.014Z",354,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[371,372,373,374],{"slug":61,"name":65,"color":48},{"slug":73,"name":74,"color":48},{"slug":67,"name":68,"color":48},{"slug":255,"name":256,"color":48},{"assessments":76},{"slug":361},{"id":378,"slug":379,"title":380,"subtitle":48,"excerpt":381,"coverUrl":382,"locale":13,"readingMinutes":383,"publishedAt":384,"viewCount":385,"likeCount":19,"commentCount":19,"author":386,"vertical":387,"topic":388,"tags":389,"_count":396,"playground":397,"hasQuiz":17,"hasPlayground":17},"01a066ea-291a-77d8-aef4-d310318f61a9","css-user-invalid-native-form-validation-timing","Your Required Field Turns Red Before You Type Anything","A signup form's email input shows a red border the instant the page loads — before the visitor has typed a single character. The obvious CSS causes it, and the obvious fix is a native pseudo-class nobody reaches for first.","\u002Fmedia\u002Fcovers\u002Fcss-user-invalid-native-form-validation-timing.png",5,"2026-09-08T10:56:30.073Z",249,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[390,391,394,395],{"slug":61,"name":65,"color":48},{"slug":392,"name":393,"color":48},"forms","Forms",{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"assessments":76},{"slug":379},{"id":399,"slug":400,"title":401,"subtitle":48,"excerpt":402,"coverUrl":403,"locale":13,"readingMinutes":51,"publishedAt":404,"viewCount":405,"likeCount":19,"commentCount":19,"author":406,"vertical":407,"topic":408,"tags":409,"_count":414,"playground":415,"hasQuiz":17,"hasPlayground":17},"01a061c4-985b-758b-86a4-21f6b2fd81f8","css-accent-color-native-form-control-tinting","One CSS Property Replaces Your Checkbox Hack","Recoloring a checkbox to match the brand usually means appearance: none, a hand-drawn checkmark, and a rebuilt focus ring. accent-color does the same job in one line — and knowing where it stops matters as much as knowing that it works.","\u002Fmedia\u002Fcovers\u002Fcss-accent-color-native-form-control-tinting.png","2026-09-07T12:02:34.132Z",280,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[410,411,412,413],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":281,"name":282,"color":48},{"assessments":76},{"slug":400},{"page":76,"perPage":240,"total":253,"totalPages":309},"\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">.modal-body\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">overflow-y\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">auto\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">overscroll-behavior-y\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">contain\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F Before — fragile, performance-hostile\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">let\u003C\u002Fspan> touchStartY \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>;\n\nmodal\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'touchstart'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>e\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  touchStartY \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> e\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>touches\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>clientY;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> passive\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\nmodal\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'touchmove'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>e\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> delta \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> touchStartY \u003Cspan class=\"shj-oper\">-\u003C\u002Fspan> e\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>touches\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>clientY;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> atBottom \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> modal\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>scrollTop \u003Cspan class=\"shj-oper\">&gt;=\u003C\u002Fspan> modal\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>scrollHeight \u003Cspan class=\"shj-oper\">-\u003C\u002Fspan> modal\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>clientHeight;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> atTop    \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> modal\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>scrollTop \u003Cspan class=\"shj-oper\">&lt;=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>atBottom \u003Cspan class=\"shj-oper\">&#38;&#38;\u003C\u002Fspan> delta \u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">||\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>atTop \u003Cspan class=\"shj-oper\">&#38;&#38;\u003C\u002Fspan> delta \u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    e\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">preventDefault\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F ← requires passive: false\n\u003C\u002Fspan>  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> passive\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">false\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F ⚠️ kills scroll performance\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F* After — one declaration, no event listener *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">.modal-body\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">overflow-y\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">auto\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">overscroll-behavior-y\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">contain\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">body\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">overscroll-behavior-y\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">none\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">.carousel\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">overflow-x\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">auto\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">overscroll-behavior-x\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">contain\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F* Both axes *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">overscroll-behavior\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">contain\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-cmnt\">\u002F* Axis-specific *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">overscroll-behavior-x\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">contain\u003C\u002Fspan>;\n\u003Cspan class=\"shj-var\">overscroll-behavior-y\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">none\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-cmnt\">\u002F* Shorthand: x-value then y-value *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">overscroll-behavior\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">contain\u003C\u002Fspan> \u003Cspan class=\"shj-num\">none\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":424},[]]