Wed, 09 Apr 2025 13:00:24 +0000
The CSS Overflow Module Level 5 specification defines a couple of new features that are designed for creating carousel UI patterns:
- Scroll Buttons: Buttons that the browser provides, as in literal
<button>
elements, that scroll the carousel content 85% of the area when clicked. - Scroll Markers: The little dots that act as anchored links, as in literal
<a>
elements that scroll to a specific carousel item when clicked.
Chrome has prototyped these features and released them in Chrome 135. Adam Argyle has a wonderful explainer over at the Chrome Developer blog. Kevin Powell has an equally wonderful video where he follows the explainer. This post is me taking notes from them.
First, some markup:
<ul class="carousel">
<li>...</li>
<li>...</li>
<li>...</li>
<li>...</li>
<li>...</li>
</ul>
First, let’s set these up in a CSS auto grid that displays the list items in a single line:
.carousel {
display: grid;
grid-auto-flow: column;
}
We can tailor this so that each list item takes up a specific amount of space, say 40%
, and insert a gap
between them:
.carousel {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 40%;
gap: 2rem;
}
This gives us a nice scrolling area to advance through the list items by moving left and right. We can use CSS Scroll Snapping to ensure that scrolling stops on each item in the center rather than scrolling right past them.
.carousel {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 40%;
gap: 2rem;
scroll-snap-type: x mandatory;
> li {
scroll-snap-align: center;
}
}
Kevin adds a little more flourish to the .carousel
so that it is easier to see what’s going on. Specifically, he adds a border
to the entire thing as well as padding
for internal spacing.
So far, what we have is a super simple slider of sorts where we can either scroll through items horizontally or click the left and right arrows in the scroller.
We can add scroll buttons to the mix. We get two buttons, one to navigate one direction and one to navigate the other direction, which in this case is left and right, respectively. As you might expect, we get two new pseudo-elements for enabling and styling those buttons:
::scroll-button(left)
::scroll-button(right)
Interestingly enough, if you crack open DevTools and inspect the scroll buttons, they are actually exposed with logical terms instead, ::scroll-button(inline-start)
and ::scroll-button(inline-end)
.

And both of those support the CSS content
property, which we use to insert a label into the buttons. Let’s keep things simple and stick with “Left” and “Right” as our labels for now:
.carousel::scroll-button(left) {
content: "Left";
}
.carousel::scroll-button(right) {
content: "Right";
}
Now we have two buttons above the carousel. Clicking them either advances the carousel left or right by 85%. Why 85%? I don’t know. And neither does Kevin. That’s just what it says in the specification. I’m sure there’s a good reason for it and we’ll get more light shed on it at some point.
But clicking the buttons in this specific example will advance the scroll only one list item at a time because we’ve set scroll snapping on it to stop at each item. So, even though the buttons want to advance by 85% of the scrolling area, we’re telling it to stop at each item.
Remember, this is only supported in Chrome at the time of writing:
We can select both buttons together in CSS, like this:
.carousel::scroll-button(left),
.carousel::scroll-button(right) {
/* Styles */
}
Or we can use the Universal Selector:
.carousel::scroll-button(*) {
/* Styles */
}
And we can even use newer CSS Anchor Positioning to set the left button on the carousel’s left side and the right button on the carousel’s right side:
.carousel {
/* ... */
anchor-name: --carousel; /* define the anchor */
}
.carousel::scroll-button(*) {
position: fixed; /* set containment on the target */
position-anchor: --carousel; /* set the anchor */
}
.carousel::scroll-button(left) {
content: "Left";
position-area: center left;
}
.carousel::scroll-button(right) {
content: "Right";
position-area: center right;
}
Notice what happens when navigating all the way to the left or right of the carousel. The buttons are disabled, indicating that you have reached the end of the scrolling area. Super neat! That’s something that is normally in JavaScript territory, but we’re getting it for free.
Let’s work on the scroll markers, or those little dots that sit below the carousel’s content. Each one is an <a>
element anchored to a specific list item in the carousel so that, when clicked, you get scrolled directly to that item.
We get a new pseudo-element for the entire group of markers called ::scroll-marker-group
that we can use to style and position the container. In this case, let’s set Flexbox on the group so that we can display them on a single line and place gaps between them in the center of the carousel’s inline size:
.carousel::scroll-marker-group {
display: flex;
justify-content: center;
gap: 1rem;
}
We also get a new scroll-marker-group
property that lets us position the group either above (before
) the carousel or below (after
) it:
.carousel {
/* ... */
scroll-marker-group: after; /* displayed below the content */
}
We can style the markers themselves with the new ::scroll-marker
pseudo-element:
.carousel {
/* ... */
> li::scroll-marker {
content: "";
aspect-ratio: 1;
border: 2px solid CanvasText;
border-radius: 100%;
width: 20px;
}
}
When clicking on a marker, it becomes the “active” item of the bunch, and we get to select and style it with the :target-current
pseudo-class:
li::scroll-marker:target-current {
background: CanvasText;
}
Take a moment to click around the markers. Then take a moment using your keyboard and appreciate that we can all of the benefits of focus states as well as the ability to cycle through the carousel items when reaching the end of the markers. It’s amazing what we’re getting for free in terms of user experience and accessibility.
We can further style the markers when they are hovered or in focus:
li::scroll-marker:hover,
li::scroll-marker:focus-visible {
background: LinkText;
}
And we can “animate” the scrolling effect by setting scroll-behavior: smooth
on the scroll snapping. Adam smartly applies it when the user’s motion preferences allow it:
.carousel {
/* ... */
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
}
Buuuuut that seems to break scroll snapping a bit because the scroll buttons are attempting to slide things over by 85% of the scrolling space. Kevin had to fiddle with his grid-auto-columns
sizing to get things just right, but showed how Adam’s example took a different sizing approach. It’s a matter of fussing with things to get them just right.
This is just a super early look at CSS Carousels. Remember that this is only supported in Chrome 135+ at the time I’m writing this, and it’s purely experimental. So, play around with it, get familiar with the concepts, and then be open-minded to changes in the future as the CSS Overflow Level 5 specification is updated and other browsers begin building support.
CSS Carousels originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
-
- 0 views
- 0 comments
Social Media
AI tools for managing, optimizing, and analyzing social media presence and campaigns.