Blossom Carousel Logo
A11y

Accessibility Guide

Build accessible carousels with semantic structure, clear controls, keyboard support, motion controls, and tested ARIA patterns.

Accessible carousels are not created by adding a single ARIA attribute. They depend on ordinary web fundamentals working together: meaningful HTML, reachable controls, predictable focus, controllable motion, visible state, and tested assistive technology output.

Blossom gives you a native scroll container and progressive drag support. The accessibility of the carousel still depends on the markup, controls, labels, and behavior you build around that container.

WCAG is a baseline, not a guarantee of usability. A carousel can pass automated checks and still be confusing if controls are unnamed, focus moves into hidden content, or screen readers hear semantics that do not match the visual pattern.

Start with the right pattern

Not every horizontal scroller needs to be announced as a carousel. Choose the lightest pattern that matches the content and behavior.

PatternUse whenRecommended structure
Scrollable listSeveral related items are visible and users can scroll through them naturally.A list such as <ul> with <li> items, plus optional previous and next buttons.
Carousel regionThe component is a distinct page feature with slides and controls.A labelled <section> or role="region" with aria-roledescription="carousel".
Tabbed carouselOnly one slide is displayed at a time and slide pickers behave exactly like tabs.A real tabs pattern with tablist, tab, and tabpanel.
Gallery or product railMultiple cards are visible at once and all cards remain normal page content.A list of articles/cards with ordinary links and buttons.

Avoid giving a component a role that promises behavior you do not implement. If the user hears "tab", arrow keys, selected state, and tabpanel relationships need to work like a tabs widget.

Use semantic slide markup

When slides are a set of related items, use a list. This gives assistive technologies a familiar count and structure.

List-based carousel
<section aria-labelledby="featured-heading">
  <h2 id="featured-heading">Featured articles</h2>

  <div class="carousel-controls" aria-label="Featured articles controls">
    <button type="button" aria-label="Previous articles">Previous</button>
    <button type="button" aria-label="Next articles">Next</button>
  </div>

  <BlossomCarousel as="ul" class="carousel">
    <li>
      <article>
        <h3>Designing with native scrolling</h3>
        <a href="/articles/native-scrolling">Read article</a>
      </article>
    </li>
    <li>
      <article>
        <h3>Understanding scroll snap</h3>
        <a href="/articles/scroll-snap">Read article</a>
      </article>
    </li>
  </BlossomCarousel>
</section>

Use a carousel role description only when the component benefits from being identified as a carousel.

Named carousel region
<section
  aria-labelledby="product-carousel-heading"
  aria-roledescription="carousel"
>
  <h2 id="product-carousel-heading">Popular products</h2>
  <!-- controls and slides -->
</section>

The accessible label should not repeat the word "carousel" when aria-roledescription="carousel" is present. A screen reader may announce the role description after the label, so "Popular products carousel, carousel" is noisy.

Provide real controls

Dragging and swiping are not enough. WCAG 2.5.7 requires functionality that uses dragging to have a non-drag alternative unless dragging is essential.

Use real buttons for carousel actions:

  • Previous slide or previous set.
  • Next slide or next set.
  • Pause and play, if the carousel auto-rotates.
  • Slide picker controls, if users can jump to a specific slide.
Carousel buttons
<button type="button" aria-label="Previous product">Previous</button>
<button type="button" aria-label="Next product">Next</button>

If multiple items are visible at once, name controls for what they actually do. "Next products" or "Next articles" can be more honest than "Next slide".

Do not disable previous and next buttons only visually. If a button cannot perform an action, use the native disabled attribute when it should be skipped in the tab order.

Disabled edge control
<button type="button" aria-label="Previous product" disabled>Previous</button>

For slide picker buttons, aria-disabled="true" can be useful for the current slide picker when users benefit from encountering the current item in the tab sequence.

Current slide picker
<div role="group" aria-label="Choose product">
  <button type="button" aria-disabled="true">Canvas tote</button>
  <button type="button">Wool scarf</button>
  <button type="button">Ceramic mug</button>
</div>

Name every control clearly

Every carousel control needs a unique accessible name. Repeated names like "Show slide" or "Read more" make the carousel hard to operate with screen readers and speech control.

Good names tell the user what will happen:

Specific control names
<button type="button" aria-label="Show canvas tote">1</button>
<button type="button" aria-label="Show wool scarf">2</button>
<button type="button" aria-label="Show ceramic mug">3</button>

If slide names are not available, use position and set size.

Position-based names
<button type="button" aria-label="Show slide 1 of 5">1</button>
<button type="button" aria-label="Show slide 2 of 5">2</button>

When visible text exists, keep the accessible name aligned with it. This helps speech recognition users activate controls by the words they can see.

Label in name
<button type="button" aria-label="Next products">Next</button>

Icon-only buttons need a name on the button. Hide the icon itself if it is decorative.

Icon-only carousel button
<button type="button" aria-label="Next product">
  <svg aria-hidden="true" focusable="false"><!-- icon --></svg>
</button>

Keep keyboard behavior predictable

Keyboard users should be able to reach the carousel, understand the controls, operate them repeatedly, and leave the component without surprise.

Expected behavior:

  • Tab and Shift + Tab move through controls and focusable slide content in DOM order.
  • Enter and Space activate carousel buttons.
  • Previous and next buttons do not move focus after activation.
  • Focus is always visible.
  • Focus does not land on content that is visually hidden or intentionally unavailable.
  • Keyboard users can access the same functionality as pointer and touch users.

You usually do not need custom scripting for Tab. Let the browser handle normal tab order unless you are implementing a real composite widget such as tabs.

Visible focus
.carousel :focus-visible,
.carousel-controls :focus-visible {
  outline: 3px solid currentColor;
  outline-offset: 3px;
}

Avoid positive tabindex values. Put controls and slides in the DOM order users should experience.

Do not trap focus in off-screen slides

Native horizontal scrolling often keeps all slides in the document flow. That is good for simple scrollable lists, but it can be confusing when a carousel presents itself as showing only one slide.

If all slides remain part of the normal content, it is acceptable for keyboard focus to move through them in order. In that case, do not claim the component behaves like tabs or like a single active panel.

If only one slide should be available at a time, inactive slides need to be unavailable to both keyboard and assistive technology users. Depending on the implementation, use conditional rendering, hidden, inert, or another approach that removes inactive interactive content from the tab order and accessibility tree.

Inactive slide removed
<div role="group" aria-roledescription="slide" aria-label="1 of 3">
  <h3>Canvas tote</h3>
  <a href="/products/canvas-tote">View product</a>
</div>

<div hidden>
  <h3>Wool scarf</h3>
  <a href="/products/wool-scarf">View product</a>
</div>

Do not use aria-hidden="true" on a slide that still contains focusable links or buttons. That creates focusable controls that are hidden from assistive technologies.

Handle motion and auto-rotation

Auto-rotation is one of the easiest ways to make a carousel inaccessible. It can move content before users finish reading, disorient screen reader users, and distract people with attention or vestibular disabilities.

If a carousel auto-rotates:

  • Provide a pause or stop button.
  • Put the rotation control before the rotating content in the tab order.
  • Stop rotation when keyboard focus enters the carousel.
  • Stop rotation when the pointer hovers over the carousel.
  • Do not restart rotation unless the user explicitly asks for it.
  • Keep the control label action-based, such as "Pause featured articles" and "Play featured articles".
Rotation control
<button type="button" aria-label="Pause featured articles">Pause</button>

Respect reduced motion preferences for scripted animation, transitions, and smooth scrolling.

Reduced motion
@media (prefers-reduced-motion: reduce) {
  .carousel {
    scroll-behavior: auto;
  }

  .carousel *,
  .carousel *::before,
  .carousel *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Announce changes with care

Not every carousel movement needs a live region. Over-announcing slide changes can be as disruptive as not announcing them.

Use these defaults:

  • Use aria-live="off" while a carousel is auto-rotating.
  • Use aria-live="polite" for user-initiated slide changes when the changed content is not otherwise obvious to assistive technologies.
  • Do not move focus just to announce a slide change.
  • Keep announcements short, such as "Slide 2 of 5" or the slide title.
Slide status
<p class="visually-hidden" aria-live="polite" aria-atomic="true">
  Showing wool scarf, slide 2 of 5
</p>

If focus moves to the newly shown slide content as part of a deliberate workflow, an additional live region may be redundant.

Choose picker semantics carefully

Slide pickers can be buttons or tabs. The choice matters.

Use buttons when:

  • Multiple slides can be visible at once.
  • Slides are part of a scrollable list.
  • Picker controls jump the scroll position but do not hide every other slide.
  • You want simple Tab navigation through each picker.

Use tabs only when:

  • One panel is presented at a time.
  • Each tab has a corresponding tabpanel.
  • The selected tab state is always correct.
  • Inactive tabpanels are hidden from users.
  • Arrow key behavior follows the tabs pattern.
Tabbed carousel shape
<div aria-label="Featured products" aria-roledescription="carousel">
  <div role="tablist" aria-label="Choose product">
    <button
      role="tab"
      aria-selected="true"
      aria-controls="panel-tote"
      id="tab-tote"
    >
      Canvas tote
    </button>
    <button
      role="tab"
      aria-selected="false"
      aria-controls="panel-scarf"
      id="tab-scarf"
    >
      Wool scarf
    </button>
  </div>

  <div role="tabpanel" id="panel-tote" aria-labelledby="tab-tote">
    <h3>Canvas tote</h3>
  </div>

  <div role="tabpanel" id="panel-scarf" aria-labelledby="tab-scarf" hidden>
    <h3>Wool scarf</h3>
  </div>
</div>

A scroller that merely moves content left and right is usually not a tabs widget.

Keep visual states perceivable

Carousel state should not depend on color alone.

  • Give active slide pickers a visible shape, outline, underline, icon, or text change in addition to color.
  • Ensure focus indicators have at least 3:1 contrast against adjacent colors.
  • Ensure icon buttons and disabled states remain understandable in forced colors mode.
  • Do not hide scrollbars unless an equally clear affordance and keyboard path remain.
  • Make touch targets at least 24 by 24 CSS pixels, and aim for 44 by 44 CSS pixels when space allows.
Forced colors-friendly states
.carousel-picker[aria-current="true"],
.carousel-picker[aria-disabled="true"] {
  outline: 2px solid currentColor;
}

@media (forced-colors: active) {
  .carousel-picker[aria-current="true"],
  .carousel-picker[aria-disabled="true"] {
    outline-color: Highlight;
  }
}

Automated checks are useful, but carousel accessibility depends heavily on behavior. Manual testing is essential.

Carousel test checklist:

  • The carousel has a clear accessible name when it is exposed as a region or group.
  • Previous, next, pause, play, and picker controls are real buttons or fully implement button behavior.
  • Every control has a unique accessible name.
  • All pointer and drag functionality has a keyboard alternative.
  • Tab order follows the visual and reading order.
  • Focus remains visible and is not hidden by sticky controls or clipped slide containers.
  • Previous and next controls can be activated repeatedly without focus moving unexpectedly.
  • Disabled or unavailable controls expose the correct disabled state.
  • Auto-rotation pauses on focus and hover.
  • Auto-rotation does not restart without user action.
  • Reduced motion preferences are respected.
  • Screen reader output matches the visible component: roles, names, selected states, and announcements are not misleading.
  • Off-screen or inactive slides do not create unexpected tab stops for the chosen pattern.
  • Slide picker semantics match the behavior: buttons for simple picker controls, tabs only for real tabbed panels.
  • The component works at 200 percent zoom and narrow viewport sizes.
  • Active, focus, disabled, and selected states remain visible in forced colors mode.

The most relevant WCAG success criteria for carousels are usually these:

CriteriaCarousel check
1.1.1 Non-text ContentImages, icon buttons, and thumbnails have appropriate text alternatives or accessible names.
1.3.1 Info and RelationshipsSlide structure, control groups, labels, and selected states are programmatically exposed.
1.3.2 Meaningful SequenceDOM order matches the reading and keyboard order.
1.4.1 Use of ColorActive, selected, disabled, and error states are not conveyed by color alone.
1.4.3 Contrast MinimumText inside slides and controls meets contrast requirements.
1.4.10 ReflowThe carousel works when zoomed or viewed in narrow layouts.
1.4.11 Non-text ContrastButtons, focus indicators, and active picker states have sufficient contrast.
2.1.1 KeyboardAll carousel functionality works with a keyboard.
2.1.2 No Keyboard TrapUsers can leave the carousel with normal keyboard navigation.
2.2.2 Pause, Stop, HideAuto-rotating or moving content can be paused, stopped, or hidden.
2.4.3 Focus OrderFocus moves through controls and slide content in a meaningful order.
2.4.7 Focus VisibleKeyboard focus is always visible.
2.4.11 Focus Not ObscuredFocused controls and slide links are not hidden by overlays or clipping.
2.5.3 Label in NameVisible control text is included in the accessible name.
2.5.7 Dragging MovementsDragging has a non-drag alternative.
2.5.8 Target Size MinimumControls and picker targets are large enough to operate.
3.2.1 On FocusFocusing a control does not unexpectedly change context.
4.1.2 Name, Role, ValueCustom controls expose correct names, roles, values, and states.
4.1.3 Status MessagesSlide changes are announced when appropriate without moving focus.

Useful resources