Building a CSS carousel — scroll buttons, markers and anchor positioning

CSS Carousels are pretty cool

Screenshot of the carousel on the homepage, in the 'Blog' section.

CSS Carousels are the new way to create carousels without any JS. In this post I’m exploring how they work, and combining the new scroll markers with anchor positioning and sibling-index().

Check the demo out on the home page.

Setup

I started out with a simple ul, containing li element with the blog post preview content.

<ul class="grid">
  {allPosts.map((post) => (
    <li>
      <a href={`/blog/${post.id}`}>
        <h3>{post.data.title}</h3>
        <p>{post.data.description}</p>
      </a>
    </li>
  ))}
</ul>

I added some CSS to turn it into a simple carousel, with some scroll snapping.

ul.grid {
  display: flex;
  flex-direction: row;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
}

li {
  scroll-snap-align: center;
  flex-shrink: 0;
}

Scroll Buttons

Now that the basic carousel works, we can add the new fun stuff! I started with the scroll buttons. For this I used ul::scroll-button(*). To target the left or right button, replace the * with left or right respectively.

ul::scroll-button(*) {
  background-color: transparent;
  width: 3rem;
  height: 3rem;
  position: absolute;
  position-anchor: --grid;
  border: none;
  transition: 0.15s ease-in-out;
}

As you can see, I have also used anchor positioning, to position the scroll buttons, and scroll markers, relative to the grid. On the left and right buttons I positioned them where I wanted them to be.

Scroll Markers

Now that we have the scroll buttons, we can add the scroll markers. For this I used ul::scroll-marker-group.

&::scroll-marker-group {
  position: absolute;
  position-anchor: --grid;
  top: anchor(--grid bottom);
  justify-self: anchor-center;
}

I didn’t even know justify-self: anchor-center existed, but it did and was exactly what I needed.

To target the scroll markers themself, you use li::scroll-marker. I’ve added a background image to each, and gave them a “random” rotation, based on it’s index in the list. To ensure it’s clear where we are in the list, I reduced the opacity of the scroll markers, and made the size a bit smaller.

li::scroll-marker {
  content: "" / "Scroll to post";
  background: url(/public/assets/svg/dot.svg);
  height: 2rem;
  width: 2rem;
  transform: rotate(calc(sibling-index() * 70deg));
  opacity: 0.65;
}

For the scroll marker of the active element, I increased the opacity, and made it a bit bigger.

li::scroll-marker:target-current {
  opacity: 1;
  scale: 1.1;
}

Code

You can find the full code for the CSS carousel here . Have fun!

Sources

Support

This is a new feature and is only available in Chrome and Edge 135+ at this time — May 2025.