[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"search-suggestions":44,"article-css-scroll-snap-native-carousel":90,"related-css-scroll-snap-native-carousel":286,"code:js:true:nbevzc":414,"code:css:true:12tmvaa":415,"code:css:true:gmnp1p":416,"code:css:true:1hi2xgd":417,"comments-019fff35-4908-727e-b7a3-dafd1ada4784":418},[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,[45,49,53,57,61,65,69,73,77,81,84,87],{"slug":46,"name":47,"articles":48},"webdev","Webdev",57,{"slug":50,"name":51,"articles":52},"frontend","Frontend",51,{"slug":54,"name":55,"articles":56},"javascript","Javascript",50,{"slug":58,"name":59,"articles":60},"css","Css",22,{"slug":62,"name":63,"articles":64},"typescript","Typescript",10,{"slug":66,"name":67,"articles":68},"performance","Performance",8,{"slug":70,"name":71,"articles":72},"grammar","Grammar",6,{"slug":74,"name":75,"articles":76},"react","React",5,{"slug":78,"name":79,"articles":80},"tutorial","Tutorial",4,{"slug":82,"name":83,"articles":80},"node","Node",{"slug":85,"name":86,"articles":43},"ai","Ai",{"slug":88,"name":89,"articles":43},"html","Html",{"id":91,"slug":92,"title":93,"subtitle":94,"excerpt":95,"coverUrl":96,"locale":13,"readingMinutes":80,"publishedAt":97,"viewCount":98,"likeCount":19,"commentCount":19,"author":99,"vertical":104,"topic":105,"tags":107,"_count":112,"playground":114,"body":116,"bodyMd":271,"seo":272,"translationGroupId":274,"thread":275,"assessments":277,"translations":282,"quiz":284},"019fff35-4908-727e-b7a3-dafd1ada4784","css-scroll-snap-native-carousel","I Ripped Out a Carousel Library. CSS Replaced It.",null,"A scroll listener, a debounce, and 200 lines of carousel-library glue code — replaced by three CSS properties that snap smoother than any of it did.","\u002Fmedia\u002Fcovers\u002Fcss-scroll-snap-native-carousel.png","2026-08-18T06:50:52.267Z",59,{"id":100,"name":101,"username":102,"avatarUrl":94,"headline":103},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":58,"name":106},"CSS",[108,109,110,111],{"slug":58,"name":59,"color":94},{"slug":46,"name":47,"color":94},{"slug":50,"name":51,"color":94},{"slug":78,"name":79,"color":94},{"assessments":113},1,{"slug":92,"title":115},"CSS scroll-snap — interactive playground",{"blocks":117,"version":113},[118,122,125,128,132,135,141,144,147,151,154,157,161,164,168,171,174,177,180,183,186,189,192,195,201,204,208,211,214,218,221,224,227,230,233,236,239,242,245,248,251,254,257,260,263],{"id":119,"html":120,"type":121},"b1","\u003Cp>The bug ticket said &quot;carousel feels broken on trackpad.&quot; It took me forty minutes to find the actual cause: my carousel library&#39;s \u003Ccode>scrollTo\u003C\u002Fcode> animation and the browser&#39;s own momentum scrolling were both trying to move the same element at the same time, and they disagreed.\u003C\u002Fp>","paragraph",{"id":123,"html":124,"type":121},"b2","\u003Cp>Two \u003Ccode>requestAnimationFrame\u003C\u002Fcode> loops, fighting over one \u003Ccode>scrollLeft\u003C\u002Fcode> value, several times a second.\u003C\u002Fp>",{"id":126,"html":127,"type":121},"b3","\u003Cp>I fixed it by deleting the library.\u003C\u002Fp>",{"id":129,"html":130,"text":130,"type":131,"level":31},"b4","The carousel everyone hand-rolls","heading",{"id":133,"html":134,"type":121},"b5","\u003Cp>Here&#39;s the pattern if you haven&#39;t built one recently: a flex row of cards, \u003Ccode>overflow-x: auto\u003C\u002Fcode>, and then JavaScript to make it feel like a carousel instead of a scrollbar with cards in it.\u003C\u002Fp>",{"id":136,"code":137,"type":138,"language":139,"highlight":140},"b6","function scrollToCard(index) {\n  const card = cards[index];\n  track.scrollTo({ left: card.offsetLeft, behavior: \"smooth\" });\n}\n\ntrack.addEventListener(\"scroll\", debounce(() => {\n  const nearest = findNearestCard(track.scrollLeft);\n  setActiveDot(nearest);\n}, 100));\n\nnextBtn.addEventListener(\"click\", () => scrollToCard(activeIndex + 1));","code","js",[],{"id":142,"html":143,"type":121},"b7","\u003Cp>A \u003Ccode>scroll\u003C\u002Fcode> listener to track position. A debounce so it doesn&#39;t fire on every pixel. A \u003Ccode>findNearestCard\u003C\u002Fcode> function doing \u003Ccode>offsetLeft\u003C\u002Fcode> math. And the moment a user drags with a finger or a trackpad instead of clicking your arrows, you&#39;re racing the browser&#39;s native scroll physics with your own \u003Ccode>scrollTo\u003C\u002Fcode> calls — which is exactly the bug that ate my Tuesday.\u003C\u002Fp>",{"id":145,"html":146,"type":121},"b8","\u003Cp>None of that code is \u003Cem>wrong\u003C\u002Fem>, exactly. It&#39;s just solving a problem the browser already solved, badly enough that a plugin market exists around it.\u003C\u002Fp>",{"id":148,"html":149,"text":150,"type":131,"level":31},"b9","What &quot;snap&quot; actually means to the browser","What \"snap\" actually means to the browser",{"id":152,"html":153,"type":121},"b10","\u003Cp>CSS has had scroll snapping since 2019 — Baseline, in every current browser, no vendor prefix, no polyfill. It doesn&#39;t animate a scroll position for you. It changes where the browser lets a scroll gesture \u003Cem>rest\u003C\u002Fem>.\u003C\u002Fp>",{"id":155,"html":156,"type":121},"b11","\u003Cp>Two properties do the whole job. One goes on the scroll container:\u003C\u002Fp>",{"id":158,"code":159,"type":138,"language":58,"highlight":160},"b12",".track {\n  display: flex;\n  overflow-x: auto;\n  scroll-snap-type: x mandatory;\n}",[],{"id":162,"html":163,"type":121},"b13","\u003Cp>One goes on each card:\u003C\u002Fp>",{"id":165,"code":166,"type":138,"language":58,"highlight":167},"b14",".card {\n  scroll-snap-align: center;\n}",[],{"id":169,"html":170,"type":121},"b15","\u003Cp>That&#39;s it. Drag, flick, trackpad-swipe, or scrollbar-drag the track, and the browser&#39;s own scroll physics — the momentum, the deceleration curve, the bit that feels \u003Cem>right\u003C\u002Fem> on every input device because the browser&#39;s engineers tuned it for every input device — settles on whichever card&#39;s snap point is closest. No \u003Ccode>scrollTo\u003C\u002Fcode>, no debounce, no \u003Ccode>findNearestCard\u003C\u002Fcode>.\u003C\u002Fp>",{"id":172,"html":173,"type":121},"b16","\u003Cp>Try dragging it yourself before you take the rest of my word for it.\u003C\u002Fp>",{"id":175,"html":176,"type":121},"b17","\u003C!-- playground:start -->",{"id":178,"html":179,"text":179,"type":131,"level":31},"b18","🎮 Try it yourself",{"id":181,"html":182,"type":121},"b19","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-scroll-snap-native-carousel\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":184,"html":185,"type":121},"b20","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":187,"html":188,"type":121},"b21","\u003C!-- playground:end -->",{"id":190,"html":191,"text":191,"type":131,"level":31},"b22","The strictness question the demo above just answered",{"id":193,"html":194,"type":121},"b23","\u003Cp>\u003Ccode>scroll-snap-type\u003C\u002Fcode> takes two words: an axis (\u003Ccode>x\u003C\u002Fcode>, \u003Ccode>y\u003C\u002Fcode>, or \u003Ccode>both\u003C\u002Fcode>) and a strictness (\u003Ccode>mandatory\u003C\u002Fcode> or \u003Ccode>proximity\u003C\u002Fcode>).\u003C\u002Fp>",{"id":196,"type":197,"items":198,"ordered":18},"b24","list",[199,200],"\u003Cstrong>\u003Ccode>mandatory\u003C\u002Fcode>\u003C\u002Fstrong> — the browser will \u003Cem>always\u003C\u002Fem> rest on a snap point when the scroll ends. Flick it a tiny bit, it still jumps to the nearest card.","\u003Cstrong>\u003Ccode>proximity\u003C\u002Fcode>\u003C\u002Fstrong> — the browser only snaps if you land close enough to a snap point already. Scroll to the middle of nowhere and it just... stays there.",{"id":202,"html":203,"type":121},"b25","\u003Cp>Most carousels want \u003Ccode>mandatory\u003C\u002Fcode>. Long scrolling galleries where snapping should feel like a \u003Cem>nudge\u003C\u002Fem>, not a \u003Cem>rule\u003C\u002Fem>, want \u003Ccode>proximity\u003C\u002Fcode> — think a photo grid that snaps rows into place without ever fighting a scroll that&#39;s clearly headed somewhere else.\u003C\u002Fp>",{"id":205,"html":206,"text":207,"type":131,"level":31},"b26","The part that makes people say &quot;but what about—&quot;","The part that makes people say \"but what about—\"",{"id":209,"html":210,"type":121},"b27","\u003Cp>Two objections kill &quot;just use CSS&quot; conversations before they start, and both have answers.\u003C\u002Fp>",{"id":212,"html":213,"type":121},"b28","\u003Cp>\u003Cstrong>&quot;What if I want the next card to peek in at the edge?&quot;\u003C\u002Fstrong> That&#39;s \u003Ccode>scroll-padding\u003C\u002Fcode> on the container — it insets the snap area the same way \u003Ccode>padding\u003C\u002Fcode> insets content, so a card can snap to a position that isn&#39;t flush against the edge:\u003C\u002Fp>",{"id":215,"code":216,"type":138,"language":58,"highlight":217},"b29",".track {\n  scroll-snap-type: x mandatory;\n  scroll-padding-inline: 24px;\n}",[],{"id":219,"html":220,"type":121},"b30","\u003Cp>\u003Cstrong>&quot;What if a sticky header overlaps the top of my snapped section?&quot;\u003C\u002Fstrong> That&#39;s \u003Ccode>scroll-margin\u003C\u002Fcode> on the \u003Cem>target\u003C\u002Fem>, not the container — it pads the snap area around one specific element, so \u003Ccode>scroll-margin-top\u003C\u002Fcode> on a section keeps a sticky nav from covering its heading the moment it snaps into view. Same trick as \u003Ccode>scroll-padding\u003C\u002Fcode>, applied per-target instead of per-container.\u003C\u002Fp>",{"id":222,"html":223,"type":121},"b31","\u003Cp>Neither of those needs a resize listener recalculating offsets. They&#39;re declarative, and they respond to the viewport the same way the rest of your layout does.\u003C\u002Fp>",{"id":225,"html":226,"text":226,"type":131,"level":31},"b32","Where a hand-rolled scroller still wins",{"id":228,"html":229,"type":121},"b33","\u003Cp>I&#39;m not going to pretend CSS snap subsumes every carousel. Autoplay — advancing on a timer without user input — is still your \u003Ccode>setInterval\u003C\u002Fcode>, because CSS has no concept of &quot;and now do this without being touched.&quot; Infinite loop-around (card N wrapping back to card 1 seamlessly) is still awkward in pure CSS; you either fake it with cloned edge cards or accept a hard stop. And if you need to know the \u003Cem>exact\u003C\u002Fem> scroll-snap position programmatically — say, to sync a progress bar — you&#39;re back to a \u003Ccode>scroll\u003C\u002Fcode> listener, just a much simpler one than before, since you&#39;re reading position instead of fighting it.\u003C\u002Fp>",{"id":231,"html":232,"type":121},"b34","\u003Cp>The honest split: snapping, physics, and touch\u002Ftrackpad feel are the browser&#39;s job now. Autoplay and looping are still yours.\u003C\u002Fp>",{"id":234,"html":235,"text":235,"type":131,"level":31},"b35","The one-line version",{"id":237,"html":238,"type":121},"b36","\u003Cp>If your carousel&#39;s JavaScript exists to make scrolling \u003Cem>feel\u003C\u002Fem> like snapping into place, delete it and write \u003Ccode>scroll-snap-type\u003C\u002Fcode> and \u003Ccode>scroll-snap-align\u003C\u002Fcode> instead — the browser has owned that job, well, since 2019. Keep your JavaScript for the parts CSS genuinely can&#39;t do: autoplay, looping, and reading position back out.\u003C\u002Fp>",{"id":240,"html":241,"type":121},"b37","\u003Cp>What&#39;s still sitting in your codebase doing a job the platform quietly took over? Scroll snap was mine — what&#39;s yours?\u003C\u002Fp>",{"id":243,"html":244,"type":121},"b38","\u003C!-- quiz:start -->",{"id":246,"html":247,"text":247,"type":131,"level":31},"b39","🧠 Test yourself",{"id":249,"html":250,"type":121},"b40","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-scroll-snap-native-carousel\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":252,"html":253,"type":121},"b41","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":255,"html":256,"type":121},"b42","\u003C!-- quiz:end -->",{"id":258,"type":259},"b43","divider",{"id":261,"html":262,"type":121},"b44","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":264,"type":197,"items":265,"ordered":18},"b45",[266,267,268,269,270],"⭐ \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>","💼 \u003Cstrong>LinkedIn\u003C\u002Fstrong> — \u003Ca href=\"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F\">linkedin.com\u002Fin\u002Fparsa-jiravand\u003C\u002Fa>","✉️ \u003Cstrong>Email\u003C\u002Fstrong> (work &amp; contract inquiries): \u003Ca href=\"mailto:bestpractice2026@gmail.com\">bestpractice2026@gmail.com\u003C\u002Fa>","The bug ticket said \"carousel feels broken on trackpad.\" It took me forty minutes to find the actual cause: my carousel library's `scrollTo` animation and the browser's own momentum scrolling were both trying to move the same element at the same time, and they disagreed.\n\nTwo `requestAnimationFrame` loops, fighting over one `scrollLeft` value, several times a second.\n\nI fixed it by deleting the library.\n\n## The carousel everyone hand-rolls\n\nHere's the pattern if you haven't built one recently: a flex row of cards, `overflow-x: auto`, and then JavaScript to make it feel like a carousel instead of a scrollbar with cards in it.\n\n```js\nfunction scrollToCard(index) {\n  const card = cards[index];\n  track.scrollTo({ left: card.offsetLeft, behavior: \"smooth\" });\n}\n\ntrack.addEventListener(\"scroll\", debounce(() => {\n  const nearest = findNearestCard(track.scrollLeft);\n  setActiveDot(nearest);\n}, 100));\n\nnextBtn.addEventListener(\"click\", () => scrollToCard(activeIndex + 1));\n```\n\nA `scroll` listener to track position. A debounce so it doesn't fire on every pixel. A `findNearestCard` function doing `offsetLeft` math. And the moment a user drags with a finger or a trackpad instead of clicking your arrows, you're racing the browser's native scroll physics with your own `scrollTo` calls — which is exactly the bug that ate my Tuesday.\n\nNone of that code is *wrong*, exactly. It's just solving a problem the browser already solved, badly enough that a plugin market exists around it.\n\n## What \"snap\" actually means to the browser\n\nCSS has had scroll snapping since 2019 — Baseline, in every current browser, no vendor prefix, no polyfill. It doesn't animate a scroll position for you. It changes where the browser lets a scroll gesture *rest*.\n\nTwo properties do the whole job. One goes on the scroll container:\n\n```css\n.track {\n  display: flex;\n  overflow-x: auto;\n  scroll-snap-type: x mandatory;\n}\n```\n\nOne goes on each card:\n\n```css\n.card {\n  scroll-snap-align: center;\n}\n```\n\nThat's it. Drag, flick, trackpad-swipe, or scrollbar-drag the track, and the browser's own scroll physics — the momentum, the deceleration curve, the bit that feels *right* on every input device because the browser's engineers tuned it for every input device — settles on whichever card's snap point is closest. No `scrollTo`, no debounce, no `findNearestCard`.\n\nTry dragging it yourself before you take the rest of my word for it.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-scroll-snap-native-carousel\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## The strictness question the demo above just answered\n\n`scroll-snap-type` takes two words: an axis (`x`, `y`, or `both`) and a strictness (`mandatory` or `proximity`).\n\n- **`mandatory`** — the browser will *always* rest on a snap point when the scroll ends. Flick it a tiny bit, it still jumps to the nearest card.\n- **`proximity`** — the browser only snaps if you land close enough to a snap point already. Scroll to the middle of nowhere and it just... stays there.\n\nMost carousels want `mandatory`. Long scrolling galleries where snapping should feel like a *nudge*, not a *rule*, want `proximity` — think a photo grid that snaps rows into place without ever fighting a scroll that's clearly headed somewhere else.\n\n## The part that makes people say \"but what about—\"\n\nTwo objections kill \"just use CSS\" conversations before they start, and both have answers.\n\n**\"What if I want the next card to peek in at the edge?\"** That's `scroll-padding` on the container — it insets the snap area the same way `padding` insets content, so a card can snap to a position that isn't flush against the edge:\n\n```css\n.track {\n  scroll-snap-type: x mandatory;\n  scroll-padding-inline: 24px;\n}\n```\n\n**\"What if a sticky header overlaps the top of my snapped section?\"** That's `scroll-margin` on the *target*, not the container — it pads the snap area around one specific element, so `scroll-margin-top` on a section keeps a sticky nav from covering its heading the moment it snaps into view. Same trick as `scroll-padding`, applied per-target instead of per-container.\n\nNeither of those needs a resize listener recalculating offsets. They're declarative, and they respond to the viewport the same way the rest of your layout does.\n\n## Where a hand-rolled scroller still wins\n\nI'm not going to pretend CSS snap subsumes every carousel. Autoplay — advancing on a timer without user input — is still your `setInterval`, because CSS has no concept of \"and now do this without being touched.\" Infinite loop-around (card N wrapping back to card 1 seamlessly) is still awkward in pure CSS; you either fake it with cloned edge cards or accept a hard stop. And if you need to know the *exact* scroll-snap position programmatically — say, to sync a progress bar — you're back to a `scroll` listener, just a much simpler one than before, since you're reading position instead of fighting it.\n\nThe honest split: snapping, physics, and touch\u002Ftrackpad feel are the browser's job now. Autoplay and looping are still yours.\n\n## The one-line version\n\nIf your carousel's JavaScript exists to make scrolling *feel* like snapping into place, delete it and write `scroll-snap-type` and `scroll-snap-align` instead — the browser has owned that job, well, since 2019. Keep your JavaScript for the parts CSS genuinely can't do: autoplay, looping, and reading position back out.\n\nWhat's still sitting in your codebase doing a job the platform quietly took over? Scroll snap was mine — what's yours?\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-scroll-snap-native-carousel\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\n---\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)\n- 💼 **LinkedIn** — [linkedin.com\u002Fin\u002Fparsa-jiravand](https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F)\n- ✉️ **Email** (work & contract inquiries): [bestpractice2026@gmail.com](mailto:bestpractice2026@gmail.com)",{"title":93,"canonical":273,"description":95},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-scroll-snap-native-carousel","019fff35-4908-727e-b7a3-dfff3df8ec8a",{"id":276,"locked":18},"019fff35-4930-752f-bb94-ae1242842119",[278],{"id":279,"slug":92,"title":280,"_count":281},"019fff35-496f-76d3-9073-a8f11d261368","CSS scroll-snap",{"questions":68},[283],{"locale":13,"slug":92},{"id":279,"slug":92,"title":280,"_count":285,"questionCount":68},{"questions":68},{"items":287,"meta":412},[288,300,318,338,356,376,394],{"id":91,"slug":92,"title":93,"subtitle":94,"excerpt":95,"coverUrl":96,"locale":13,"readingMinutes":80,"publishedAt":97,"viewCount":289,"likeCount":19,"commentCount":19,"author":290,"vertical":291,"topic":292,"tags":293,"_count":298,"playground":299,"hasQuiz":17,"hasPlayground":17},60,{"id":100,"name":101,"username":102,"avatarUrl":94,"headline":103},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":58,"name":106},[294,295,296,297],{"slug":58,"name":59,"color":94},{"slug":46,"name":47,"color":94},{"slug":50,"name":51,"color":94},{"slug":78,"name":79,"color":94},{"assessments":113},{"slug":92},{"id":301,"slug":302,"title":303,"subtitle":94,"excerpt":304,"coverUrl":305,"locale":13,"readingMinutes":80,"publishedAt":306,"viewCount":307,"likeCount":19,"commentCount":113,"author":308,"vertical":309,"topic":310,"tags":311,"_count":316,"playground":317,"hasQuiz":17,"hasPlayground":17},"019ffa10-eeed-707d-9b50-7d83664c7a9a","css-trigonometric-functions-radial-layout","Stop Writing Math.cos() in a Resize Listener","CSS learned trigonometry. Here's the radial menu that used to need a resize listener — and doesn't anymore.","\u002Fmedia\u002Fcovers\u002Fcss-trigonometric-functions-radial-layout.png","2026-08-17T07:03:57.776Z",269,{"id":100,"name":101,"username":102,"avatarUrl":94,"headline":103},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":58,"name":106},[312,313,314,315],{"slug":58,"name":59,"color":94},{"slug":46,"name":47,"color":94},{"slug":54,"name":55,"color":94},{"slug":50,"name":51,"color":94},{"assessments":113},{"slug":302},{"id":319,"slug":320,"title":321,"subtitle":94,"excerpt":322,"coverUrl":323,"locale":13,"readingMinutes":72,"publishedAt":324,"viewCount":325,"likeCount":19,"commentCount":19,"author":326,"vertical":327,"topic":328,"tags":329,"_count":336,"playground":337,"hasQuiz":17,"hasPlayground":17},"019ff73a-ed77-732e-a13f-8be6d6939332","calc-size-animate-to-auto","I Can Finally Delete My max-height: 9999px Animation Hack","CSS transitions have never been able to animate to height: auto, because auto isn't a number the engine can interpolate. calc-size() and interpolate-size finally give it real numbers — no JavaScript, no magic-number ceiling.","\u002Fmedia\u002Fcovers\u002Fcalc-size-animate-to-auto.png","2026-08-16T06:46:49.217Z",231,{"id":100,"name":101,"username":102,"avatarUrl":94,"headline":103},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":58,"name":106},[330,331,332,333],{"slug":58,"name":59,"color":94},{"slug":46,"name":47,"color":94},{"slug":50,"name":51,"color":94},{"slug":334,"name":335,"color":94},"animation","Animation",{"assessments":113},{"slug":320},{"id":339,"slug":340,"title":341,"subtitle":94,"excerpt":342,"coverUrl":343,"locale":13,"readingMinutes":80,"publishedAt":344,"viewCount":345,"likeCount":19,"commentCount":19,"author":346,"vertical":347,"topic":348,"tags":349,"_count":354,"playground":355,"hasQuiz":17,"hasPlayground":17},"019ff19b-f763-70e0-abdf-eded59c8e69a","overscroll-behavior-scroll-containment","You're blocking touchmove events to contain scroll. `overscroll-behavior` does it natively.","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","2026-08-12T07:38:53.786Z",333,{"id":100,"name":101,"username":102,"avatarUrl":94,"headline":103},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":58,"name":106},[350,351,352,353],{"slug":58,"name":59,"color":94},{"slug":46,"name":47,"color":94},{"slug":50,"name":51,"color":94},{"slug":54,"name":55,"color":94},{"assessments":113},{"slug":340},{"id":357,"slug":358,"title":359,"subtitle":94,"excerpt":360,"coverUrl":361,"locale":13,"readingMinutes":80,"publishedAt":362,"viewCount":363,"likeCount":19,"commentCount":19,"author":364,"vertical":365,"topic":366,"tags":367,"_count":374,"playground":375,"hasQuiz":17,"hasPlayground":17},"019fe660-e5d0-756a-9ae2-20c6ff4c2f48","css-style-queries-react-to-custom-properties","Container queries can check more than size — style queries let components read custom property values","Size queries react to container width. Style queries react to CSS custom property values on a container ancestor — enabling component-level theming and variant switching in pure CSS, no JavaScript involved.","\u002Fmedia\u002Fcovers\u002Fcss-style-queries-react-to-custom-properties.png","2026-08-06T08:44:20.136Z",162,{"id":100,"name":101,"username":102,"avatarUrl":94,"headline":103},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":58,"name":106},[368,369,370,371],{"slug":58,"name":59,"color":94},{"slug":46,"name":47,"color":94},{"slug":50,"name":51,"color":94},{"slug":372,"name":373,"color":94},"components","Components",{"assessments":113},{"slug":358},{"id":377,"slug":378,"title":379,"subtitle":94,"excerpt":380,"coverUrl":381,"locale":13,"readingMinutes":76,"publishedAt":382,"viewCount":383,"likeCount":19,"commentCount":19,"author":384,"vertical":385,"topic":386,"tags":387,"_count":392,"playground":393,"hasQuiz":17,"hasPlayground":17},"019fe660-d6d5-7099-89bb-af60e48fcb28","content-visibility-skip-offscreen-rendering","Your browser renders everything, even what you can't see — `content-visibility: auto` fixes that","Browsers lay out and paint the entire page on load, including the 80% below the fold. `content-visibility: auto` tells the browser to skip that work for off-screen elements — and it's Baseline 2024.","\u002Fmedia\u002Fcovers\u002Fcontent-visibility-skip-offscreen-rendering.png","2026-08-04T08:44:59.227Z",374,{"id":100,"name":101,"username":102,"avatarUrl":94,"headline":103},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":58,"name":106},[388,389,390,391],{"slug":58,"name":59,"color":94},{"slug":46,"name":47,"color":94},{"slug":66,"name":67,"color":94},{"slug":50,"name":51,"color":94},{"assessments":113},{"slug":378},{"id":395,"slug":396,"title":397,"subtitle":94,"excerpt":398,"coverUrl":399,"locale":13,"readingMinutes":76,"publishedAt":400,"viewCount":401,"likeCount":19,"commentCount":19,"author":402,"vertical":403,"topic":404,"tags":405,"_count":410,"playground":411,"hasQuiz":17,"hasPlayground":17},"019fe660-c020-760c-b4c7-46c1465e25bd","css-light-dark-function","You're writing dark-mode colors twice. `light-dark()` fixes that.","Every `@media (prefers-color-scheme: dark)` block duplicates your color definitions. `light-dark()` lets you write both values on the same line, in the same rule, with no separate block and no build step. Baseline 2024.","\u002Fmedia\u002Fcovers\u002Fcss-light-dark-function.png","2026-08-02T08:29:56.698Z",289,{"id":100,"name":101,"username":102,"avatarUrl":94,"headline":103},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":58,"name":106},[406,407,408,409],{"slug":58,"name":59,"color":94},{"slug":46,"name":47,"color":94},{"slug":50,"name":51,"color":94},{"slug":54,"name":55,"color":94},{"assessments":113},{"slug":396},{"page":113,"perPage":413,"total":60,"totalPages":80},7,"\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">scrollToCard\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>index\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> card \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> cards\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>index\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>;\n  track\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">scrollTo\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> left\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> card\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>offsetLeft\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> behavior\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"smooth\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\ntrack\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"scroll\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-func\">debounce\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\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> nearest \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">findNearestCard\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>track\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>scrollLeft\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-func\">setActiveDot\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>nearest\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-num\">100\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\nnextBtn\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"click\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">scrollToCard\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>activeIndex \u003Cspan class=\"shj-oper\">+\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">.track\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">display\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">flex\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">overflow-x\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">auto\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">scroll-snap-type\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">x\u003C\u002Fspan> \u003Cspan class=\"shj-num\">mandatory\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-var\">.card\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">scroll-snap-align\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">center\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\">.track\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">scroll-snap-type\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">x\u003C\u002Fspan> \u003Cspan class=\"shj-num\">mandatory\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">scroll-padding-inline\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">24\u003C\u002Fspan>\u003Cspan class=\"shj-var\">px\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":419},[]]