Skip to content

Carousel

A carousel built the platform’s way: the track is a native CSS scroll-snap strip, so it scrolls and snaps with zero JavaScript — by touch, trackpad, scrollbar, and (because the track is a focusable scroll region) the arrow keys. The optional enhanceCarousel behavior back-fills the discoverable controls: prev/next buttons, a dot strip, active tracking, and inerting of off-screen slides.

Live example
<div class="re-carousel frame" data-re-carousel>
  <div
    class="re-carousel__track"
    role="group"
    aria-roledescription="carousel"
    aria-label="Featured photos"
    tabindex="0"
  >
    <figure
      class="re-carousel__slide"
      aria-roledescription="slide"
      aria-label="Sunset over the bay"
    >
      <div class="slide-art" style="background-color: var(--re-color-accent-600)">1</div>
      <figcaption>Sunset over the bay</figcaption>
    </figure>
    <figure
      class="re-carousel__slide"
      aria-roledescription="slide"
      aria-label="Forest trail in autumn"
    >
      <div class="slide-art" style="background-color: var(--re-color-success-600)">2</div>
      <figcaption>Forest trail in autumn</figcaption>
    </figure>
    <figure
      class="re-carousel__slide"
      aria-roledescription="slide"
      aria-label="City skyline at dusk"
    >
      <div class="slide-art" style="background-color: var(--re-color-warning-600)">3</div>
      <figcaption>City skyline at dusk</figcaption>
    </figure>
    <figure
      class="re-carousel__slide"
      aria-roledescription="slide"
      aria-label="Desert dunes at noon"
    >
      <div class="slide-art" style="background-color: var(--re-color-danger-600)">4</div>
      <figcaption>Desert dunes at noon</figcaption>
    </figure>
  </div>
</div>

Add data-re-carousel-autoplay (optionally ="3000" for the interval in ms, default 5000) to auto-advance. The behavior injects a Pause/Play button — the WCAG 2.2.2 stop mechanism — and pauses on hover, on focus within the carousel, and when the tab is hidden. Under prefers-reduced-motion: reduce it starts paused. It works on either control rung (the timer + pause button are JS regardless of who draws the dots).

Live example
<div class="re-carousel frame" data-re-carousel data-re-carousel-autoplay="3000">
  <div
    class="re-carousel__track"
    role="group"
    aria-roledescription="carousel"
    aria-label="Product highlights (auto-advancing)"
    tabindex="0"
  >
    <figure class="re-carousel__slide" aria-roledescription="slide" aria-label="Fast">
      <div class="slide-art" style="background-color: var(--re-color-accent-600)">A</div>
      <figcaption>Fast</figcaption>
    </figure>
    <figure class="re-carousel__slide" aria-roledescription="slide" aria-label="Accessible">
      <div class="slide-art" style="background-color: var(--re-color-success-600)">B</div>
      <figcaption>Accessible</figcaption>
    </figure>
    <figure class="re-carousel__slide" aria-roledescription="slide" aria-label="Native">
      <div class="slide-art" style="background-color: var(--re-color-warning-600)">C</div>
      <figcaption>Native</figcaption>
    </figure>
  </div>
</div>
  • Rung A (no JS).re-carousel__track is overflow-x: auto + scroll-snap-type: inline mandatory. It already works: drag, swipe, scrollbar, and Arrow/Page/Home/End once the track has focus. This is the whole carousel for users without the behavior.
  • Rung C (enhanceCarousel) — injects the buttons + dots and tracks the active slide. This is the accessibility-tested control path.
  • Rung B (CSS Carousel pseudo-elements)::scroll-button() / ::scroll-marker draw the controls with no JS at all. It’s shipped behind @supports and active where the browser supports it (Chrome 135+ today); enhanceCarousel feature-tests the same condition and stands down, so you never get both control sets. It’s labeled experimental: the UA markers/buttons are ahead of assistive tech (documented tab-exposure bugs), so Rung C stays the a11y-tested path and the manual SR pass is done against it.
  • The scroll region is focusabletabindex="0" on the track makes the native arrow-key scrolling reachable (a scrollable region must be keyboard-operable). The buttons are the primary, discoverable control; arrow-key scrolling is the secondary path.
  • Buttons, not tabs — prev/next and the dots are plain <button>s with aria-label + aria-current (the active dot), not a role="tablist" — the scroll model isn’t a tabpanel.
  • Off-screen slides are inert — the behavior sets inert on any slide that doesn’t overlap the track viewport (geometry-based, so it holds at any slide-per-view count) so Tab can’t land on a slide scrolled out of view (the classic carousel keyboard trap), and they leave the reading order. Prev/next at the ends use aria-disabled (not the native disabled property), so they stay focusable and announced instead of dropping focus to the page.
  • Settle announcement — a polite live region announces the settled slide (“Sunset over the bay (1 of 4)”), debounced to fire once per snap rather than on every scroll frame; pausing autoplay also announces where it stopped.
  • Autoplay is stoppable — auto-advancing content needs a way to pause it (WCAG 2.2.2 Pause, Stop, Hide). The injected Pause/Play button is that mechanism; the carousel also pauses while hovered, while focus is inside it, and while the tab is hidden, and it announces nothing while playing (so the live region isn’t spammed). Under prefers-reduced-motion: reduce it starts paused.
  • Reduced motion — programmatic scrolling drops to an instant jump under prefers-reduced-motion: reduce (and the CSS scroll-behavior: smooth is already gated on it).
  • RTL — navigation uses scrollIntoView({ inline }) and active detection uses box geometry, so neither depends on the sign of scrollLeft (which differs across engines).
  • Forced colors — the active dot is re-established with Highlight and the buttons get a real ButtonText border (their surface + shadow flatten away).
  • No custom event — derive the index from the native scroll yourself if you need it. Dynamic slides are out of scope for now: re-run enhanceCarousel after adding or removing slides.

See the accessibility guide for the project-wide approach.