Eric Niquette

Why make this?

While I was developing the first version of this website, one of the goals I had set for myself was to avoid the use of JavaScript if I could help it, and keep it lean. In my search for CSS-based carousels, I came across several really impressive projects but I only needed very basic functionality.

While simple, this technique is lightweight and provides a good amount of creative flexibility while remaining completely free of JavaScript, albeit with some annoying caveats and limitations.

Demo

The following is an example of the carousel's basic functionality with the addition of coloured scrollbar sections. Once you've transferred focus to one of the slides, you can press the arrow keys to navigate or use the scrollbar by dragging the thumb or by clicking on individual segments. If you're on mobile, you can tap the scrollbar sections or swipe through the slides.

After transferring focus to the carousel, you can navigate using the arrow keys, the segments on the scrollbar, or by scrolling the bar or swiping on mobile.

Note that if you're using Firefox or a browser not based on the WebKit or Blink engine, you probably won't see a custom scrollbar and likely will only see a coloured bar. It should otherwise not impact the general functionality.

A Chromium update has unfortunately broken some of the the features this technique relies on. See Chromium bug 1409053 for more information. Until then, please keep in mind that the demo may not function as expected.

See the Pen CSS-only carousel using the scrollbar by Eric Niquette (@ericniquette) on CodePen.

How it works

The basic premise involves placing a set of slides off-screen in an overflowed container, which then be brought into view by scrolling. With the help of the scroll-snap-type property, the scrollbar gently snaps to the center of the slides. The scrollbar can then be customized to blend in with the rest of the design and serves as the navigation.

Accessibility

As far as I can tell, this should be reasonably accessible. You can navigate through the slides using the arrow keys, tab through links, and the content is not excluded from the DOM so it's entirely accessible to screen readers. When transferring focus to a slide, it scrolls into view and doesn't break or override default browser behavior in any way.

It can, however, be difficult to understand that scrolling is required, particularly if the scrollbar's design isn't too intuitive. This is especially true in Firefox due to the browser's modern and thin design scrollbar design. This could potentially be mitigated by an icon, a "swipe me" animation, or some other explanatory text if desired.

Most browsers will automatically transfer focus to scrolling elements, though you could manually set a tabindex="0" value to the container to meet WCAG Success Criterion 2.1.1.

Caveats

Given its minimal nature, there are a few limitations to this solution:

  • Scrollbar customization is a Blink and WebKit feature, so it should work on Chrome, Edge, Brave, etc. Firefox, however, does not support full customization and instead supports the WCAG specs, which are — by comparison — rather restrictive. While it doesn't break functionality, it certainly does make it less visually attractive.
  • Mobile browsers do not allow "picking up" and dragging scrollbar thumbs. This can cause scrollbars to feel counterintuitive if styled improperly. It does, however, natively allow tapping of the track to move forward and back, as well as swiping through individual slides.
  • You cannot skip directly to a specific entry using the scrollbar and must cycle through the previous entries individually to reach a specific item. In other words, you can't jump from the first to the third slide without scrolling past the second. While it is possible to provide direct links to specific slides, this unfortunately populates an entry to the browser's history.
  • Scrollbars have a lot of design limitations. For example, you cannot specify a value for the width of the bar. Instead, we have to rely on margins to trim the sides, which can be a little fiddly.
  • You cannot infinitely scroll through the items and automatically loop back. It's a linear start-to-end scroll that stops at the last entry.

Solution

We need to create a series of slides that we'll be cycling through. You can add as many as you need but keep in mind that the larger the total width, the smaller the scrollbar's thumb will be.

<div class="carousel">
  <div class="slide" id="slide1">
    <h2>Slide 1</h2>
  </div>
  <div class="slide" id="slide2">
    <h2>Slide 2</h2>
  </div>
  <div class="slide" id="slide3">
    <h2>Slide 3</h2>
  </div>
  <div class="slide" id="slide4">
    <h2>Slide 4</h2>
  </div>
</div>

Using the flex display mode, we can have them line up side by side. The flex-wrap: nowrap property prevents them from breaking to new lines. The overflow property will create a scrolling area to accommodate the contents' width.

By adding scroll-snap-type, the browser will to snap to the elements. With scroll-snap-align, we set the desired location of that snap to the center of the slide.

.carousel {
  display: flex;
  flex-wrap: nowrap;
  gap: 5%;
  overflow-x: scroll;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  }

.slide {
  box-sizing: border-box;
  flex-shrink: 0;
  margin-bottom: 1rem;
  scroll-snap-align: center;
  width: 100%;
  }

Now that we have content scrolling in a box, we can customize the scrollbar to blend in with the website's UI. For non webkit-based browsers, the best we can do is set the scrollbar-color property.

It's worth noting that the scrollbar's length can only be controlled by using margins. I'm using fairly random values for the sake of this example so you'll need to customize it to suit your layout's width.

.carousel {
  scrollbar-color: #AA6262 transparent;
  }

.carousel::-webkit-scrollbar {
  height: 2rem;
  }

  .carousel::-webkit-scrollbar-track {
    background: gainsboro;
    padding: 0.625rem;
    margin: 0 10vw; /* Customize this value */
    }

  .carousel::-webkit-scrollbar-thumb {
    background-color: slategrey;
    border: 0.5rem solid gainsboro;
    }

The complete CSS should look something like this.

.carousel {
  display: flex;
  flex-wrap: nowrap;
  gap: 5%;
  overflow-x: scroll;
  scrollbar-color: #AA6262 transparent;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  }

  .carousel::-webkit-scrollbar {
    height: 2rem;
    }

    .carousel::-webkit-scrollbar-track {
      background: gainsboro;
      padding: 0.625rem;
      margin: 0 10vw;
      }

    .carousel::-webkit-scrollbar-thumb {
      background-color: slategrey;
      border: 0.5rem solid gainsboro;
      }

.slide {
  box-sizing: border-box;
  flex-shrink: 0;
  margin-bottom: 1rem;
  scroll-snap-align: center;
  width: 100%;
  }

Customization

Adding space between slides

You can control the whitespace that appears between individual slides as you scroll with the gap attribute.

If you do not want to have a gap, keep in mind that when your container scales to a decimal value you may see a ghostly half-pixel line of the previous slide as your move through the entries. It's really only apparent when your slides have strong contrasting colours, but something to keep in mind.

.carousel { gap: 2rem; }

Scrollbar segments

The scrollbar can be divided into coloured segments using a gradient background with overlapping start and stop points, creating solid lines. It can be finnicky to initially set up, but it does provide visual breakpoints.

The border on the scrollbar track makes the bar smaller by blending with the background colour, whereas the one on the thumb increases its size for a bit of extra contrast.

.carousel::-webkit-scrollbar-track {
  border-top: 0.5rem solid white;
  border-bottom: 0.5rem solid white;
  background: linear-gradient(
    to right,
    linen 25%,
    lightsalmon 25%,
    lightsalmon 50%,
    lightblue 50%,
    lightblue 75%,
    gainsboro 75%
    );
  }

.carousel::-webkit-scrollbar-thumb {
  border: 0.5rem solid slategrey;
  }

Markers

To mimic the look of a classic carousel, we can add bullets or markers to the scrollbar with a little bit of work. The basic idea is to place a series of background images — that look like list bullets — to the scrollbar's track at specific intervals.

Note that this positioning is rarely pixel-perfect; it'll usually end up pixel or less off. Instead of trying to get it to align to a fraction of a percent, I use a slightly larger image as my thumb than I do in the background to compensate and hide the gap.

.carousel::-webkit-scrollbar-track {
  background-image: url('dot.svg'), url('dot.svg'), url('dot.svg'), url('dot.svg');
  background-repeat: no-repeat;
  background-position: 10%, 37%, 63%, 90%; /* Customize these values */
  }

.carousel::-webkit-scrollbar-thumb {
  background: url('marker.svg') center no-repeat;
  }

The images dot.svg represent normal bullets and marker.svg the larger, currently-active bullet which replaces the scrollbar's thumb button.