Accessibility Guide
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.
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.
| Pattern | Use when | Recommended structure |
|---|---|---|
| Scrollable list | Several 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 region | The component is a distinct page feature with slides and controls. | A labelled <section> or role="region" with aria-roledescription="carousel". |
| Tabbed carousel | Only 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 rail | Multiple 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.
<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.
<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.
<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.
<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.
<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:
<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.
<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.
<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.
<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:
TabandShift + Tabmove through controls and focusable slide content in DOM order.EnterandSpaceactivate 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.
.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.
<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".
<button type="button" aria-label="Pause featured articles">Pause</button>
Respect reduced motion preferences for scripted animation, transitions, and smooth scrolling.
@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.
<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
Tabnavigation through each picker.
Use tabs only when:
- One panel is presented at a time.
- Each
tabhas a correspondingtabpanel. - The selected tab state is always correct.
- Inactive tabpanels are hidden from users.
- Arrow key behavior follows the tabs pattern.
<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.
.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;
}
}
Test the carousel as a component
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.
Taborder 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.
Carousel WCAG map
The most relevant WCAG success criteria for carousels are usually these:
| Criteria | Carousel check |
|---|---|
| 1.1.1 Non-text Content | Images, icon buttons, and thumbnails have appropriate text alternatives or accessible names. |
| 1.3.1 Info and Relationships | Slide structure, control groups, labels, and selected states are programmatically exposed. |
| 1.3.2 Meaningful Sequence | DOM order matches the reading and keyboard order. |
| 1.4.1 Use of Color | Active, selected, disabled, and error states are not conveyed by color alone. |
| 1.4.3 Contrast Minimum | Text inside slides and controls meets contrast requirements. |
| 1.4.10 Reflow | The carousel works when zoomed or viewed in narrow layouts. |
| 1.4.11 Non-text Contrast | Buttons, focus indicators, and active picker states have sufficient contrast. |
| 2.1.1 Keyboard | All carousel functionality works with a keyboard. |
| 2.1.2 No Keyboard Trap | Users can leave the carousel with normal keyboard navigation. |
| 2.2.2 Pause, Stop, Hide | Auto-rotating or moving content can be paused, stopped, or hidden. |
| 2.4.3 Focus Order | Focus moves through controls and slide content in a meaningful order. |
| 2.4.7 Focus Visible | Keyboard focus is always visible. |
| 2.4.11 Focus Not Obscured | Focused controls and slide links are not hidden by overlays or clipping. |
| 2.5.3 Label in Name | Visible control text is included in the accessible name. |
| 2.5.7 Dragging Movements | Dragging has a non-drag alternative. |
| 2.5.8 Target Size Minimum | Controls and picker targets are large enough to operate. |
| 3.2.1 On Focus | Focusing a control does not unexpectedly change context. |
| 4.1.2 Name, Role, Value | Custom controls expose correct names, roles, values, and states. |
| 4.1.3 Status Messages | Slide changes are announced when appropriate without moving focus. |
Useful resources
- WAI Carousels Tutorial - accessible carousel structure, functionality, animation, and styling guidance.
- ARIA Authoring Practices Guide: Carousel Pattern - expected carousel roles, states, properties, and keyboard behavior.
- ARIA Authoring Practices Guide: Tabs Pattern - use this when slide pickers are truly tabs.
- WCAG 2.2 Quick Reference - success criteria, techniques, and filters.