{"id":7830,"date":"2025-11-26T16:30:50","date_gmt":"2025-11-26T21:30:50","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=7830"},"modified":"2025-11-26T16:30:51","modified_gmt":"2025-11-26T21:30:51","slug":"how-to-add-and-remove-items-from-a-native-css-carousel-with-css","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/how-to-add-and-remove-items-from-a-native-css-carousel-with-css\/","title":{"rendered":"How to Add and Remove Items From a Native CSS Carousel (&#8230;with CSS)"},"content":{"rendered":"\n<p>The <a href=\"https:\/\/www.w3.org\/TR\/css-overflow-5\/\">CSS Overflow Module Level 5<\/a> defines specs for scrolling controls that enable users to navigate overflow content without manually scrolling (like click-and-dragging the scrollbar, the trackpad, a scrollwheel, or the like). This includes scroll <em>buttons<\/em>, which enable users to scroll 85% of the scrollport, unless scroll snapping is enabled, as well as scroll <em>markers<\/em>, which enable users to skip to specific scroll targets (direct children of the scroll container).<\/p>\n\n\n\n<p>These buttons and markers make themselves present via CSS pseudo-elements. At the time of this writing, these pseudo-elements are only supported in Chrome 142+:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code><strong>::scroll-marker<\/strong><\/code>: a generated element that links to a scroll target in a scroll container (behaves like an <code>&lt;a&gt;<\/code>)<\/li>\n\n\n\n<li><code><strong>::scroll-button<\/strong>(&lt;direction&gt;)<\/code>: a generated element that scrolls 85% of the scrollport, where <code>&lt;direction&gt;<\/code> can be <code>up<\/code> , <code>down<\/code>, <code>left<\/code>, <code>right<\/code>, or <code>all<\/code> (behaves like a <code>&lt;button&gt;<\/code>)<\/li>\n<\/ul>\n\n\n\n<p>There are <em>many<\/em> ways that we can leverage these CSS features. I\u2019ll share some of them throughout this article, but focus on one in particular: a standard CSS carousel. We&#8217;ll use all the bells and whistles mentioned above and one extra twist, the ability to <strong>add and remove items<\/strong> from it.<\/p>\n\n\n\n<p>This functionality would be ideal for showing product photos according to user-defined variables such as color or size, or showing items handpicked by users, just to give to two examples.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_RNrZrNm\" src=\"\/\/codepen.io\/anon\/embed\/RNrZrNm?height=450&amp;theme-id=1&amp;slug-hash=RNrZrNm&amp;default-tab=css,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed RNrZrNm\" title=\"CodePen Embed RNrZrNm\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Ready to dig in?<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-1-setting-up-the-component-and-scroll-container\">Step 1: Setting up the Scroll Container<\/h2>\n\n\n\n<p>In this first step, I\u2019m just going to walk you through the HTML, the carousel\u2019s dimensions, and how we determine which carousel items have been added to the carousel.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The HTML<\/h3>\n\n\n\n<p>The carousel itself is an unordered list (<code>&lt;ul&gt;<\/code>) with list items (<code>&lt;li&gt;<\/code>) inside (in terms of accessibility, I think this is the best markup). Prior to that we have some checkboxes, which users can toggle to add and remove the carousel items, and for the purpose of this walkthrough I\u2019ve pre-selected a few of them using the <code>checked<\/code> attribute. Finally, we wrap all of that, establishing our overall component. This part is important because we\u2019ll be seeing which checkboxes <em>within<\/em> it <em>aren\u2019t<\/em> checked, and then hiding the corresponding carousel items \u2014 also within it \u2014 based on that:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"component\"<\/span>&gt;<\/span>\n\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"checkbox\"<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"i1\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span> <span class=\"hljs-attr\">for<\/span>=<span class=\"hljs-string\">\"i1\"<\/span>&gt;<\/span>Toggle slide 1<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"checkbox\"<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"i2\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span> <span class=\"hljs-attr\">for<\/span>=<span class=\"hljs-string\">\"i2\"<\/span>&gt;<\/span>Toggle slide 2<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"checkbox\"<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"i3\"<\/span> <span class=\"hljs-attr\">checked<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span> <span class=\"hljs-attr\">for<\/span>=<span class=\"hljs-string\">\"i3\"<\/span>&gt;<\/span>Toggle slide 3<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"checkbox\"<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"i4\"<\/span> <span class=\"hljs-attr\">checked<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span> <span class=\"hljs-attr\">for<\/span>=<span class=\"hljs-string\">\"i4\"<\/span>&gt;<\/span>Toggle slide 4<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"checkbox\"<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"i5\"<\/span> <span class=\"hljs-attr\">checked<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span> <span class=\"hljs-attr\">for<\/span>=<span class=\"hljs-string\">\"i5\"<\/span>&gt;<\/span>Toggle slide 5<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">ul<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"carousel\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">li<\/span>&gt;<\/span>1<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">li<\/span>&gt;<\/span>2<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">li<\/span>&gt;<\/span>3<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">li<\/span>&gt;<\/span>4<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">li<\/span>&gt;<\/span>5<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">ul<\/span>&gt;<\/span>\n\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">The CSS<\/h3>\n\n\n\n<p>First we rein in the carousel\u2019s width using <code>max-width<\/code>, then we make the height half of whatever the computed width is using <code>aspect-ratio<\/code>. Just a little responsive design. <\/p>\n\n\n\n<p>After that, we see which checkboxes aren\u2019t checked, and then hide the corresponding scroll targets\/carousel items. For example, this is what we do for the first checkbox and carousel item:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>.component:has(input:nth-of-type(1):not(:checked)) {}<\/code>: select the component, which contains an input, the first of which isn\u2019t checked<\/li>\n\n\n\n<li><code>li:nth-of-type(1) {}<\/code>: within that, select the first carousel item<\/li>\n\n\n\n<li><code>display: hidden<\/code>: and hide it<\/li>\n<\/ul>\n\n\n\n<p>That covers the adding-and-removing logic. What\u2019s even better is that those checkboxes can be used to submit data, so if you were to mark up the component as a <code>&lt;form&gt;<\/code>, you do form-like things with it, like serialize the data and save it. <\/p>\n\n\n\n<p>In the next section, we declare placeholder styles for when <em>no<\/em> checkboxes are checked (<code>.component:not(:has(input:checked))<\/code>). This conditional <code>:has()<\/code> block (and certain others) isn\u2019t required, but it\u2019s a great way of clarifying what, when, and why, for other developers and for yourself if you come back to the code later.<\/p>\n\n\n\n<p>If at least one checkbox is checked (<code>.component:has(input:checked)<\/code>), the carousel receives <code>display: flex<\/code>, which makes the carousel items flow horizontally, while the carousel items within receive <code>min-width: 100%<\/code>, which ensures that only one carousel item is displayed at a time.<\/p>\n\n\n\n<p>The final block runs if multiple checkboxes are checked (<code>.component:has(input:checked ~ input:checked)<\/code>, which translates to \u201cif a checked checkbox is immediately or non-immediately followed by another checked checkbox\u201d). This is where the code for the scroll markers, scroll buttons, and scroll behaviors will go.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.component<\/span> {\n  ul.carousel {\n    <span class=\"hljs-attribute\">max-width<\/span>: <span class=\"hljs-number\">738px<\/span>;\n    <span class=\"hljs-attribute\">aspect-ratio<\/span>: <span class=\"hljs-number\">2<\/span> \/ <span class=\"hljs-number\">1<\/span>;\n  }\n\n  <span class=\"hljs-comment\">\/* If the first checkbox isn\u2019t checked *\/<\/span>\n  &amp;<span class=\"hljs-selector-pseudo\">:has(input<\/span><span class=\"hljs-selector-pseudo\">:nth-of-type(1)<\/span><span class=\"hljs-selector-pseudo\">:not(<\/span><span class=\"hljs-selector-pseudo\">:checked))<\/span> {\n    <span class=\"hljs-comment\">\/* Hide the first list item *\/<\/span>\n    <span class=\"hljs-attribute\">li<\/span>:<span class=\"hljs-built_in\">nth-of-type<\/span>(<span class=\"hljs-number\">1<\/span>) {\n      display: none;\n    }\n  }\n\n  <span class=\"hljs-comment\">\/* And so on, incrementing the nth-of-type *\/<\/span>\n  &amp;<span class=\"hljs-selector-pseudo\">:has(input<\/span><span class=\"hljs-selector-pseudo\">:nth-of-type(2)<\/span><span class=\"hljs-selector-pseudo\">:not(<\/span><span class=\"hljs-selector-pseudo\">:checked))<\/span> {\n    <span class=\"hljs-attribute\">li<\/span>:<span class=\"hljs-built_in\">nth-of-type<\/span>(<span class=\"hljs-number\">2<\/span>) {\n      display: none;\n    }\n  }\n\n  <span class=\"hljs-comment\">\/* If no checkboxes are checked *\/<\/span>\n  &amp;<span class=\"hljs-selector-pseudo\">:not(<\/span><span class=\"hljs-selector-pseudo\">:has(input<\/span><span class=\"hljs-selector-pseudo\">:checked))<\/span> {\n    <span class=\"hljs-comment\">\/* Placeholder content for the carousel *\/<\/span>\n    ul.carousel {\n      <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-number\">#eee<\/span>;\n    }\n  }\n\n  <span class=\"hljs-comment\">\/* If any checkbox is checked *\/<\/span>\n  &amp;<span class=\"hljs-selector-pseudo\">:has(input<\/span><span class=\"hljs-selector-pseudo\">:checked)<\/span> {\n    ul.carousel {\n      <span class=\"hljs-comment\">\/* Implies flex-direction:row *\/<\/span>\n      <span class=\"hljs-attribute\">display<\/span>: flex;\n\n      li {\n        <span class=\"hljs-comment\">\/* Show one list item only *\/<\/span>\n        <span class=\"hljs-attribute\">min-width<\/span>: <span class=\"hljs-number\">100%<\/span>;\n      }\n    }\n  }\n\n  <span class=\"hljs-comment\">\/* If multiple are checked *\/<\/span>\n  &amp;<span class=\"hljs-selector-pseudo\">:has(input<\/span><span class=\"hljs-selector-pseudo\">:checked<\/span> ~ <span class=\"hljs-selector-tag\">input<\/span><span class=\"hljs-selector-pseudo\">:checked)<\/span> {\n    <span class=\"hljs-comment\">\/* Step 2 and step 3 code here *\/<\/span>\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_xbVxXPe\" src=\"\/\/codepen.io\/anon\/embed\/xbVxXPe?height=450&amp;theme-id=1&amp;slug-hash=xbVxXPe&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed xbVxXPe\" title=\"CodePen Embed xbVxXPe\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-2-building-the-carousel-and-scroll-markers\">Step 2: Building the Carousel and Scroll Markers<\/h2>\n\n\n\n<p>Continuing from where we left off, let\u2019s revisit the carousel. Keep in mind that we\u2019re working within the context of multiple checkboxes being checked (<code>.component:has(input:checked ~ input:checked)<\/code>, which means that the carousel items that are visible will horizontally overflow the carousel unless we declare <code>overflow: hidden<\/code> (or <code>overflow: scroll<\/code> if you want to allow manual scrolling).<\/p>\n\n\n\n<p>Next, by default, scroll buttons enable users to scroll 85% of the scrollport, but we\u2019re looking for a slideshow-type behavior where one complete slide is shown at a time, so we\u2019ll need to set up scroll snapping for the additional 15%.&nbsp;<code>scroll-snap-type: x<\/code>&nbsp;will do exactly that for the x-axis. We\u2019ll figure out the exact alignment in a moment.<\/p>\n\n\n\n<p>Complimenting that, <code>scroll-behavior: smooth<\/code> will ensure that users snap to the carousel items smoothly when using the scroll buttons (and scroll markers).<\/p>\n\n\n\n<p><code>anchor-name: --carousel<\/code> turns the carousel into <a href=\"https:\/\/frontendmasters.com\/blog\/tag\/anchor\/\">an anchor,<\/a> naming it <code>--carousel<\/code>. This will enable us to align the scroll buttons (and scroll markers) relative to the carousel, but again this is something that we\u2019ll do in a moment.<\/p>\n\n\n\n<p>The <code>scroll-marker-group<\/code> property relates to the <code>::scroll-marker-group<\/code> pseudo-element that\u2019s generated whenever a scroll marker is generated. It\u2019s basically the container for the scroll markers, where the value of <code>scroll-marker-group<\/code> determines whether <code>::scroll-marker-group<\/code> is inserted before or after the carousel\u2019s content (similarly to <code>::before<\/code>\/<code>::after<\/code>), affecting tab order. You <strong>must<\/strong> set <code>scroll-marker-group<\/code> to either <code>before<\/code> or <code>after<\/code>.<\/p>\n\n\n\n<p>Treat <code>::scroll-marker-group<\/code> like any other container. For example, <code>display: flex; gap: 1rem;<\/code> will make the scroll markers flow horizontally with <code>1rem<\/code> of spacing between them. After that, we combine <code>position: fixed<\/code> and <code>position-anchor: --carousel<\/code> (<code>--carousel<\/code> refers to the anchor that we named earlier) to align the container relative to the carousel, then <code>justify-self: anchor-center<\/code> to align it horizontally and <code>bottom: calc(anchor(bottom) + 1rem)<\/code> to align it <code>1rem<\/code> from the bottom.<\/p>\n\n\n\n<p>The scroll markers are pseudo-elements of the scroll targets (so <code>li::scroll-marker<\/code>), which makes sense, right? One marker for every scroll target. But as mentioned before, they\u2019re inserted into <code>::scroll-marker-group<\/code>, <em>not<\/em> the scroll targets, so <em>where<\/em> you select them isn\u2019t where they\u2019re inserted. After your brain has reconciled that (it took me a minute), you\u2019ll need to set their <code>content<\/code> property. We\u2019re using stylized markers here, so we\u2019ve set them to an empty string (<code>content: \"\"<\/code>), but you can insert whatever content you want inside of them, and even number them using <a href=\"https:\/\/codepen.io\/mrdanielschwarz\/pen\/yyyeWep\">CSS counters<\/a>.<\/p>\n\n\n\n<p>After that you\u2019re free to style them, and if you want to take that a step further, <code>::scroll-marker<\/code> has three pseudo-classes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>:target-current<\/code>: the active scroll marker<\/li>\n\n\n\n<li><code>:target-before<\/code>: all scroll markers before the active one<\/li>\n\n\n\n<li><code>:target-after<\/code>: all scroll markers after the active one<\/li>\n<\/ul>\n\n\n\n<p>Note: the pseudo-class <strong>must<\/strong> be prefixed by the pseudo-element:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-comment\">\/* Won\u2019t work *\/<\/span>\n<span class=\"hljs-selector-pseudo\">:target-current<\/span> {\n  <span class=\"hljs-comment\">\/* ... *\/<\/span>\n}\n\n<span class=\"hljs-comment\">\/* Won\u2019t work (even though it should?) *\/<\/span>\n<span class=\"hljs-selector-pseudo\">::scroll-marker<\/span> {\n  &amp;:target-current {\n    <span class=\"hljs-comment\">\/* ... *\/<\/span>\n  }\n}\n\n<span class=\"hljs-comment\">\/* Only this will work *\/<\/span>\n<span class=\"hljs-selector-pseudo\">::scroll-marker<\/span><span class=\"hljs-selector-pseudo\">:target-current<\/span> {\n  <span class=\"hljs-comment\">\/* ... *\/<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This is the full (step 2) CSS code:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-comment\">\/* Step 1 code here *\/<\/span>\n\n<span class=\"hljs-selector-tag\">ul<\/span><span class=\"hljs-selector-class\">.carousel<\/span> {\n  <span class=\"hljs-comment\">\/* Hide overflow\/disable scrolling *\/<\/span>\n  <span class=\"hljs-attribute\">overflow<\/span>: hidden;\n\n  <span class=\"hljs-comment\">\/* Enable x-axis scroll snapping *\/<\/span>\n  <span class=\"hljs-attribute\">scroll-snap-type<\/span>: x;\n\n  <span class=\"hljs-comment\">\/* Enable smooth scrolling *\/<\/span>\n  <span class=\"hljs-attribute\">scroll-behavior<\/span>: smooth;\n\n  <span class=\"hljs-comment\">\/* Turn the carousel into an anchor *\/<\/span>\n  <span class=\"hljs-attribute\">anchor-name<\/span>: --carousel;\n\n  <span class=\"hljs-comment\">\/* Insert the SMG after the content *\/<\/span>\n  <span class=\"hljs-attribute\">scroll-marker-group<\/span>: after;\n\n  <span class=\"hljs-comment\">\/* SMG (holds the scroll markers) *\/<\/span>\n  &amp;::scroll-marker-group {\n    <span class=\"hljs-comment\">\/* Scroll marker layout *\/<\/span>\n    <span class=\"hljs-attribute\">display<\/span>: flex;\n    <span class=\"hljs-attribute\">gap<\/span>: <span class=\"hljs-number\">1rem<\/span>;\n\n    <span class=\"hljs-comment\">\/* Anchor the SMG to the carousel *\/<\/span>\n    <span class=\"hljs-attribute\">position<\/span>: fixed;\n    <span class=\"hljs-attribute\">position-anchor<\/span>: --carousel;\n\n    <span class=\"hljs-comment\">\/* Anchor it horizontally *\/<\/span>\n    <span class=\"hljs-attribute\">justify-self<\/span>: anchor-center;\n\n    <span class=\"hljs-comment\">\/* Anchor it near the bottom *\/<\/span>\n    <span class=\"hljs-attribute\">bottom<\/span>: <span class=\"hljs-built_in\">calc<\/span>(anchor(bottom) + <span class=\"hljs-number\">1rem<\/span>);\n  }\n\n  <span class=\"hljs-selector-tag\">li<\/span><span class=\"hljs-selector-pseudo\">::scroll-marker<\/span> {\n    <span class=\"hljs-comment\">\/* Generate empty markers *\/<\/span>\n    <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-string\">\"\"<\/span>;\n\n    <span class=\"hljs-comment\">\/* Style the markers *\/<\/span>\n    <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">1rem<\/span>;\n    <span class=\"hljs-attribute\">aspect-ratio<\/span>: <span class=\"hljs-number\">1<\/span> \/ <span class=\"hljs-number\">1<\/span>;\n  }\n\n  <span class=\"hljs-comment\">\/* Active marker *\/<\/span>\n  <span class=\"hljs-selector-tag\">li<\/span><span class=\"hljs-selector-pseudo\">::scroll-marker<\/span><span class=\"hljs-selector-pseudo\">:target-current<\/span> {\n    <span class=\"hljs-attribute\">background<\/span>: white;\n  }\n\n  <span class=\"hljs-comment\">\/* All markers before the active one *\/<\/span>\n  <span class=\"hljs-selector-tag\">li<\/span><span class=\"hljs-selector-pseudo\">::scroll-marker<\/span><span class=\"hljs-selector-pseudo\">:target-before<\/span> {\n    <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">hsl<\/span>(from white h s l \/ <span class=\"hljs-number\">50%<\/span>);\n  }\n\n  <span class=\"hljs-comment\">\/* All markers after the active one *\/<\/span>\n  <span class=\"hljs-selector-tag\">li<\/span><span class=\"hljs-selector-pseudo\">::scroll-marker<\/span><span class=\"hljs-selector-pseudo\">:target-after<\/span> {\n    <span class=\"hljs-attribute\">background<\/span>: red;\n  }\n\n  <span class=\"hljs-comment\">\/* Step 3 code here *\/<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_ZYWErza\" src=\"\/\/codepen.io\/anon\/embed\/ZYWErza?height=550&amp;theme-id=1&amp;slug-hash=ZYWErza&amp;default-tab=result\" height=\"550\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed ZYWErza\" title=\"CodePen Embed ZYWErza\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-3-adding-the-scroll-buttons\">Step 3: adding the scroll buttons<\/h2>\n\n\n\n<p>In this final step we\u2019ll add the scroll buttons, which are pseudo-elements of the scroll container (the carousel in this case). <code>::scroll-button()<\/code> accepts five physical values for its only parameter:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>::scroll-button(*)<\/code><\/li>\n\n\n\n<li><code>::scroll-button(left)<\/code><\/li>\n\n\n\n<li><code>::scroll-button(right)<\/code><\/li>\n\n\n\n<li><code>::scroll-button(up)<\/code><\/li>\n\n\n\n<li><code>::scroll-button(down)<\/code><\/li>\n<\/ul>\n\n\n\n<p>As well as four logical values:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>::scroll-button(block-start)<\/code><\/li>\n\n\n\n<li><code>::scroll-button(block-end)<\/code><\/li>\n\n\n\n<li><code>::scroll-button(inline-start)<\/code><\/li>\n\n\n\n<li><code>::scroll-button(inline-end)<\/code><\/li>\n<\/ul>\n\n\n\n<p>They too must have valid <code>content<\/code> properties like the scroll markers, otherwise they won\u2019t show up. In today\u2019s example we\u2019re only using <code>::scroll-button(left)<\/code> and <code>::scroll-button(right)<\/code>, inserting directional arrows into them, but only when enabled (e.g., <code>::scroll-button(left):enabled<\/code>). When they\u2019re <code>:disabled<\/code> (which means that it\u2019s impossible to scroll any further in that direction), no <code>content<\/code> property is set (which, again, means that they won\u2019t show up).<\/p>\n\n\n\n<p>We also use anchor positioning again, to align the scroll buttons relative to the carousel. <code>::scroll-button(*)<\/code> selects all scroll buttons, which is where most of this logic is declared, then of course <code>::scroll-button(left)<\/code> and <code>::scroll-button(right)<\/code> to align the individual buttons to their respective edges.<\/p>\n\n\n\n<p>And finally, we also declare <code>scroll-snap-align: center<\/code> on the carousel items (<code>&lt;li&gt;<\/code>), complimenting the <code>scroll-snap-type: x<\/code> that we declared on the carousel earlier, which ensures that when users click on these scroll buttons, they don\u2019t scroll 85% of the scrollport. Instead, they snap to the scroll target fully.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-comment\">\/* Step 1 code here *\/<\/span>\n\n<span class=\"hljs-selector-tag\">ul<\/span><span class=\"hljs-selector-class\">.carousel<\/span> {\n  <span class=\"hljs-comment\">\/* Step 2 code here *\/<\/span>\n\n  <span class=\"hljs-comment\">\/* All scroll buttons *\/<\/span>\n  &amp;::scroll-button(*) {\n    <span class=\"hljs-comment\">\/* Anchor them to the carousel *\/<\/span>\n    <span class=\"hljs-attribute\">position<\/span>: fixed;\n    <span class=\"hljs-attribute\">position-anchor<\/span>: --carousel;\n\n    <span class=\"hljs-comment\">\/* Anchor them vertically *\/<\/span>\n    <span class=\"hljs-attribute\">align-self<\/span>: anchor-center;\n  }\n\n  <span class=\"hljs-comment\">\/* Left scroll button (if enabled) *\/<\/span>\n  &amp;<span class=\"hljs-selector-pseudo\">::scroll-button(left)<\/span><span class=\"hljs-selector-pseudo\">:enabled<\/span> {\n    <span class=\"hljs-comment\">\/* Generate the button with content *\/<\/span>\n    <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-string\">\"\u2b05\ufe0e\"<\/span>;\n\n    <span class=\"hljs-comment\">\/* Anchor it near the left *\/<\/span>\n    <span class=\"hljs-attribute\">left<\/span>: <span class=\"hljs-built_in\">calc<\/span>(anchor(left) + <span class=\"hljs-number\">1rem<\/span>);\n  }\n\n  <span class=\"hljs-comment\">\/* Right scroll button (if enabled) *\/<\/span>\n  &amp;<span class=\"hljs-selector-pseudo\">::scroll-button(right)<\/span><span class=\"hljs-selector-pseudo\">:enabled<\/span> {\n    <span class=\"hljs-comment\">\/* Generate the button with content *\/<\/span>\n    <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-string\">\"\u2b95\"<\/span>;\n\n    <span class=\"hljs-comment\">\/* Anchor it near the right *\/<\/span>\n    <span class=\"hljs-attribute\">right<\/span>: <span class=\"hljs-built_in\">calc<\/span>(anchor(right) + <span class=\"hljs-number\">1rem<\/span>);\n  }\n\n  <span class=\"hljs-selector-tag\">li<\/span> {\n    <span class=\"hljs-comment\">\/*\n      Snap to the center of scroll targets\n      instead of scrolling 85% of the scrollport\n    *\/<\/span>\n    <span class=\"hljs-attribute\">scroll-snap-align<\/span>: center;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_RNrZrNm\" src=\"\/\/codepen.io\/anon\/embed\/RNrZrNm?height=550&amp;theme-id=1&amp;slug-hash=RNrZrNm&amp;default-tab=result\" height=\"550\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed RNrZrNm\" title=\"CodePen Embed RNrZrNm\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>And here\u2019s the same thing but vertical:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_gbrOvNW\" src=\"\/\/codepen.io\/anon\/embed\/gbrOvNW?height=550&amp;theme-id=1&amp;slug-hash=gbrOvNW&amp;default-tab=result\" height=\"550\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed gbrOvNW\" title=\"CodePen Embed gbrOvNW\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"wrapping-up\">Wrapping up<\/h2>\n\n\n\n<p>These features are really cool. They can be used in so many different ways, from <a href=\"https:\/\/codepen.io\/mrdanielschwarz\/pen\/wBBMEKV\">tabs<\/a> to <a href=\"https:\/\/codepen.io\/mrdanielschwarz\/pen\/yyyeWep\">pagination<\/a>. To throw in a more a real-world example of what we explored here today, a carousel showing product photos:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_dPMyeWK\" src=\"\/\/codepen.io\/anon\/embed\/dPMyeWK?height=550&amp;theme-id=1&amp;slug-hash=dPMyeWK&amp;default-tab=result\" height=\"550\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed dPMyeWK\" title=\"CodePen Embed dPMyeWK\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Just choose the color that you want and that\u2019s what the carousel will show!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>It&#8217;s already quite impressive you can build a carousel with no JS at all (in Chrome, for now, anyway) and with some checkbox-hack stuff we can control dynamically what is shown.<\/p>\n","protected":false},"author":37,"featured_media":7887,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[341,125,7],"class_list":["post-7830","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-carousels","tag-checkbox","tag-css"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/carousels.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7830","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/37"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=7830"}],"version-history":[{"count":11,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7830\/revisions"}],"predecessor-version":[{"id":7891,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7830\/revisions\/7891"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/7887"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=7830"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=7830"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=7830"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}