Accessibility pattern · Disclosure and content

Carousel

The Pause button comes first, rotation waits while focus or the pointer is inside, and it never starts by itself when the system asks for reduced motion. Slides are labelled groups, and the live region is off while the carousel rotates on its own, so only the changes someone asks for are announced.

WCAG criteria
7
Keyboard rules
4
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabReaches Pause first, then the slide's link, Previous, the slide picker and Next. Focus anywhere but Pause holds the rotation.
Enter or SpaceOn Pause, stops the rotation; the button becomes Play, which starts it again. Focus stays on the button.
Enter or SpaceOn Previous or Next, shows the slide before or after, wrapping at the ends; focus stays on the button.
Enter or SpaceOn a picker button, shows that slide.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the carouselThis week's collections, carousel. Pause slide show, button
Pause is pressedPlay slide show, button (the new name; some screen readers say it only when focus comes back)
Next is pressed2 of 5: Festive silks
Focus reaches a picker buttonFestive silks, button, current
Reading into a slide2 of 5, slide
The carousel moves on by itselfNothing: the live region is off while it rotates

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<section class="ap-carousel" data-ap-carousel aria-roledescription="carousel" aria-labelledby="car-label">
  <div class="ap-carousel__top">
    <h3 class="ap-carousel__label" id="car-label">This week's collections</h3>
    <button type="button" class="ap-carousel__rotate" data-car-rotate aria-label="Pause slide show">
      <svg class="ap-carousel__icon ap-carousel__icon--pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6v12M15 6v12"/></svg>
      <svg class="ap-carousel__icon ap-carousel__icon--play" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5.5v13l11-6.5Z"/></svg>
      <span class="ap-carousel__rotate-text">Pause</span>
    </button>
  </div>

  <div class="ap-carousel__slides" id="car-slides">
      <div class="ap-carousel__slide ap-carousel__slide--rain is-current" role="group" aria-roledescription="slide" aria-label="1 of 5" id="car-slide-1">
        <div class="ap-carousel__art" aria-hidden="true">
          <svg viewBox="0 0 320 200" preserveAspectRatio="xMidYMid slice" focusable="false">
            <path class="ap-carousel__ink ap-carousel__ink--soft" d="M196 60a26 26 0 0 1 50-6 20 20 0 1 1 6 39h-58a17 17 0 1 1 2-33Z"/>
            <path class="ap-carousel__line" d="M206 112l-8 18M226 112l-8 18M246 112l-8 18M216 140l-8 18M236 140l-8 18"/>
            <rect class="ap-carousel__ink" x="40" y="118" width="120" height="22" rx="4"/>
            <rect class="ap-carousel__ink ap-carousel__ink--soft" x="48" y="96" width="104" height="22" rx="4"/>
            <rect class="ap-carousel__ink" x="56" y="74" width="88" height="22" rx="4"/>
            <path class="ap-carousel__line ap-carousel__line--thin" d="M40 129h120M48 107h104M56 85h88"/>
          </svg>
        </div>
        <div class="ap-carousel__copy">
          <p class="ap-carousel__kicker">New this week</p>
          <h4 class="ap-carousel__name" id="car-name-1">Monsoon cottons</h4>
          <p class="ap-carousel__text">Light handloom cottons from Bengal and Andhra Pradesh that dry fast, from ₹1,450.</p>
          <a class="ap-carousel__link" href="#car-name-1">Shop monsoon cottons<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
        </div>
      </div>
      <div class="ap-carousel__slide ap-carousel__slide--silk" role="group" aria-roledescription="slide" aria-label="2 of 5" id="car-slide-2">
        <div class="ap-carousel__art" aria-hidden="true">
          <svg viewBox="0 0 320 200" preserveAspectRatio="xMidYMid slice" focusable="false">
            <path class="ap-carousel__ink ap-carousel__ink--soft" d="M30 170a130 130 0 0 1 260 0Z"/>
            <path class="ap-carousel__ink" d="M70 170a90 90 0 0 1 180 0Z"/>
            <path class="ap-carousel__ink ap-carousel__ink--soft" d="M110 170a50 50 0 0 1 100 0Z"/>
            <path class="ap-carousel__ink" d="M160 26l9 14-9 14-9-14ZM60 60l7 11-7 11-7-11ZM260 60l7 11-7 11-7-11Z"/>
          </svg>
        </div>
        <div class="ap-carousel__copy">
          <p class="ap-carousel__kicker">Wedding season</p>
          <h4 class="ap-carousel__name" id="car-name-2">Festive silks</h4>
          <p class="ap-carousel__text">Kanjeevaram and Banarasi silks with real zari borders, from ₹8,900.</p>
          <a class="ap-carousel__link" href="#car-name-2">Shop festive silks<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
        </div>
      </div>
      <div class="ap-carousel__slide ap-carousel__slide--block" role="group" aria-roledescription="slide" aria-label="3 of 5" id="car-slide-3">
        <div class="ap-carousel__art" aria-hidden="true">
          <svg viewBox="0 0 320 200" preserveAspectRatio="xMidYMid slice" focusable="false">
            <defs>
              <pattern id="car-block" width="80" height="80" y="20" patternUnits="userSpaceOnUse">
                <path class="ap-carousel__ink" d="M32.5 28a7.5 7.5 0 1 0 15 0a7.5 7.5 0 1 0 -15 0ZM44.5 40a7.5 7.5 0 1 0 15 0a7.5 7.5 0 1 0 -15 0ZM32.5 52a7.5 7.5 0 1 0 15 0a7.5 7.5 0 1 0 -15 0ZM20.5 40a7.5 7.5 0 1 0 15 0a7.5 7.5 0 1 0 -15 0Z"/>
                <path class="ap-carousel__hole" d="M34.5 40a5.5 5.5 0 1 0 11 0a5.5 5.5 0 1 0 -11 0Z"/>
                <path class="ap-carousel__ink ap-carousel__ink--soft" d="M-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0ZM77 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0ZM-3 80a3 3 0 1 0 6 0a3 3 0 1 0 -6 0ZM77 80a3 3 0 1 0 6 0a3 3 0 1 0 -6 0Z"/>
              </pattern>
            </defs>
            <rect width="320" height="200" fill="url(#car-block)"/>
          </svg>
        </div>
        <div class="ap-carousel__copy">
          <p class="ap-carousel__kicker">Made in Jaipur</p>
          <h4 class="ap-carousel__name" id="car-name-3">Hand block prints</h4>
          <p class="ap-carousel__text">Kurtas and dupattas stamped by hand in natural dyes, from ₹1,200.</p>
          <a class="ap-carousel__link" href="#car-name-3">Shop block prints<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
        </div>
      </div>
      <div class="ap-carousel__slide ap-carousel__slide--hills" role="group" aria-roledescription="slide" aria-label="4 of 5" id="car-slide-4">
        <div class="ap-carousel__art" aria-hidden="true">
          <svg viewBox="0 0 320 200" preserveAspectRatio="xMidYMid slice" focusable="false">
            <circle class="ap-carousel__ink ap-carousel__ink--soft" cx="236" cy="58" r="22"/>
            <path class="ap-carousel__ink ap-carousel__ink--soft" d="M0 150l60-62 46 40 58-70 74 92 82-56v106H0Z"/>
            <path class="ap-carousel__ink" d="M0 170c40-18 80-18 120 0s80 18 120 0 60-18 80-10v40H0Z"/>
          </svg>
        </div>
        <div class="ap-carousel__copy">
          <p class="ap-carousel__kicker">From Kashmir</p>
          <h4 class="ap-carousel__name" id="car-name-4">Pashmina shawls</h4>
          <p class="ap-carousel__text">Hand-spun and hand-woven, each with a certificate of origin, from ₹6,500.</p>
          <a class="ap-carousel__link" href="#car-name-4">Shop pashmina shawls<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
        </div>
      </div>
      <div class="ap-carousel__slide ap-carousel__slide--gift" role="group" aria-roledescription="slide" aria-label="5 of 5" id="car-slide-5">
        <div class="ap-carousel__art" aria-hidden="true">
          <svg viewBox="0 0 320 200" preserveAspectRatio="xMidYMid slice" focusable="false">
            <rect class="ap-carousel__ink ap-carousel__ink--soft" x="58" y="92" width="96" height="78" rx="6"/>
            <rect class="ap-carousel__ink" x="52" y="78" width="108" height="22" rx="5"/>
            <path class="ap-carousel__line" d="M106 78v92M106 78c-10-22-34-22-30-6 2 6 18 6 30 6Zm0 0c10-22 34-22 30-6-2 6-18 6-30 6Z"/>
            <rect class="ap-carousel__ink" x="182" y="112" width="78" height="58" rx="6"/>
            <path class="ap-carousel__line ap-carousel__line--light" d="M221 112v58M182 138h78"/>
          </svg>
        </div>
        <div class="ap-carousel__copy">
          <p class="ap-carousel__kicker">Ready to give</p>
          <h4 class="ap-carousel__name" id="car-name-5">Gifts under ₹2,000</h4>
          <p class="ap-carousel__text">Stoles, pouches and table linen, wrapped for free with a handwritten note.</p>
          <a class="ap-carousel__link" href="#car-name-5">Shop gifts<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
        </div>
      </div>
  </div>

  <div class="ap-carousel__nav">
    <button type="button" class="ap-carousel__step" data-car-prev aria-controls="car-slides" aria-label="Previous slide">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m15 6-6 6 6 6"/></svg>
    </button>
    <div class="ap-carousel__dots" role="group" aria-label="Choose a slide">
      <button type="button" class="ap-carousel__dot" aria-labelledby="car-name-1" aria-controls="car-slide-1" aria-current="true"><span class="ap-carousel__pip" aria-hidden="true"></span></button>
      <button type="button" class="ap-carousel__dot" aria-labelledby="car-name-2" aria-controls="car-slide-2"><span class="ap-carousel__pip" aria-hidden="true"></span></button>
      <button type="button" class="ap-carousel__dot" aria-labelledby="car-name-3" aria-controls="car-slide-3"><span class="ap-carousel__pip" aria-hidden="true"></span></button>
      <button type="button" class="ap-carousel__dot" aria-labelledby="car-name-4" aria-controls="car-slide-4"><span class="ap-carousel__pip" aria-hidden="true"></span></button>
      <button type="button" class="ap-carousel__dot" aria-labelledby="car-name-5" aria-controls="car-slide-5"><span class="ap-carousel__pip" aria-hidden="true"></span></button>
    </div>
    <button type="button" class="ap-carousel__step" data-car-next aria-controls="car-slides" aria-label="Next slide">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9 6 6 6-6 6"/></svg>
    </button>
  </div>
  <p class="ap-carousel__live" aria-live="off" aria-atomic="true"></p>
</section>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 2.2.2 Pause, Stop, Hide Level A

    The slides move for more than five seconds, so Pause is the first control, and rotation also holds while the pointer or focus is inside.

  • 2.1.1 Keyboard Level A

    Pause, Previous, Next and every slide in the picker work with Enter or Space.

  • 2.4.3 Focus Order Level A

    Pause comes first in the carousel's tab order, and no control moves focus when it is used.

  • 2.3.3 Animation from Interactions Level AAA

    When the system asks for reduced motion, the carousel starts paused and slides change without the fade.

  • 4.1.2 Name, Role, Value Level A

    The region and slides carry roles and roledescriptions, the picker marks the shown slide with aria-current, and the rotation button's name says what it will do.

  • 4.1.3 Status Messages Level AA

    Slide changes someone asks for are announced from a polite live region; changes the timer makes are not.

  • 2.5.8 Target Size (Minimum) Level AA

    The picker dots are small, but each button around them is a target of at least 24 by 24 pixels.

Usage

When to use it

Use it

  • A few promotions of equal weight in limited space, where people are happy to see only the first.
  • Browsing a small, visual set, like collections or featured stories, with the reader in control.

Use something else

  • Anything people must see: most people never get past the first slide.
  • Content that needs reading time; motion competes with reading, so show it in a grid or a list.
  • More than five or six slides: use a grid of cards.

Common failures

How it usually goes wrong

  • No way to stop it

    Slides that move by themselves for more than five seconds need a pause control. Pause on hover alone fails keyboard and touch users.

  • Pause at the end

    If the stop control comes after the slides, keyboard users tab through moving content to reach it. Here it is the first stop.

  • Announcing every automatic change

    A live region that is always polite reads out a new slide every few seconds, over whatever the person is reading. It is off while rotating.

  • Hidden slides still in the tab order

    Slides moved off screen keep their links reachable, so focus vanishes into content nobody can see. Slides not shown here are visibility: hidden.

  • Dots with no names

    Picker buttons announced only as "button" tell nobody which slide they show. Each one here is named by its slide's heading.

  • Autoplay that ignores reduced motion

    Moving content can cause nausea and distraction. When the system asks for less motion, the carousel starts paused.

Notes

Building it

  • The timer pauses rather than restarts on hover or focus, and the bar on the current dot pauses with it, so a slide never jumps the moment the pointer leaves.
  • Focus on the Pause button itself does not hold the rotation, so pressing Play starts it at once; focus anywhere else inside does.
  • Six seconds a slide is a floor, not a target. Slides with more words need longer, or no autoplay at all.
  • Name the carousel without the word "carousel": aria-roledescription already says it.
  • The APG's grouped example marks the shown slide's picker button with aria-disabled; aria-current is used here, so it is announced as current rather than unavailable.

Sources: WAI-ARIA Authoring Practices: Carousel · WCAG 2.2 Understanding 2.2.2: Pause, Stop, Hide

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction