Search topics
Carousels that keyboard and screen reader users can't use
A homepage carousel's arrows and dots take only clicks, or, once they are buttons, still never say which slide is showing or that it changed.
<div class="carousel-next"></div> The problem
Keyboard users can't show any slide but the first, and even once the arrows and dots work, screen reader users aren't told which slide is showing or that it changed.
A city museum opens its homepage with a carousel of three slides: the autumn exhibition, late opening on Fridays, and membership. Each slide has a heading, a line of text and a link. An arrow at each side and three dots underneath change the slide on a click, and the dot for the slide on show is filled in. The arrows and dots are divs and spans with click handlers. Pressing Tab goes from the first slide's link straight to the next link on the page, so a keyboard user only ever sees the first slide. A screen reader never hears the arrows or dots, the filled dot is only a colour, and when a mouse user changes the slide, nothing is read out.
Many carousels get halfway: the arrows and dots are real buttons with names, so the keyboard can change the slide. Even then, a screen reader user who presses Next hears nothing at all, and on the dots hears "Slide 2, button" with no word of which slide is the current one. They can control the carousel but can't tell what it is showing.
Success criterion 2.1.1 Keyboard asks that everything the mouse can do, the keyboard can do. Showing the second or third slide is done only by clicking a div or a span, which take no focus, so the keyboard can't do it at all. Success criterion 4.1.2 Name, Role, Value asks that controls expose a name, a role and their current state. The arrows and dots expose no name and no role, and the dots show which slide is current only by colour, a state that is never exposed.
Turning the arrows and dots into buttons is not enough on its own. The current dot still shows its state only by colour, which fails 4.1.2 in the same way. And pressing Next swaps the content beside the button while focus stays on it, so a screen reader user hears nothing and can't tell whether the press did anything, or which slide they are now on. The ARIA Authoring Practices carousel reads the new slide out through a polite live region, and the fix below does the same.
The carousel is the first thing on the homepage, and the slides after the first hold the late opening and the membership offer. Anyone who can't use a mouse, or can't see the dots, never learns they are there.
2 of 10 groups affected
- No vision (affected)
- Low vision (not affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (affected)
- Reach, strength (not affected)
- Cognitive (not affected)
- Seizures (not affected)
- Limited manipulation The arrows and dots take no focus, so keyboard, switch and voice control users can't show the second or third slide, or reach the links on them.
- Without vision The arrows and dots are never read, the current dot is only a filled circle, and a new slide arrives without a word, so a screen reader user can't tell there are other slides, which one is showing, or that it changed.
Arrows and dots built from divs and spans have no role, so axe has nothing to check. The audit reports them when they sit inside an element whose class names it as a carousel, slider or slideshow, have class names such as prev, next, arrow, dot or bullet, show a pointer cursor and never take focus. That covers the arrows of Swiper without its accessibility module and the dots of older Owl and Flickity versions, but not arrows a site has named some other way. When the arrows are buttons with only an icon, axe reports the missing name as button-name. No check can tell whether the current dot says it is current, or whether a new slide is read out. AI fixes often add role="button" and tabindex="0" to the divs and stop there, which gives a Tab stop that Enter and Space do nothing on; put aria-label on a div with no role, which many screen readers ignore; or add aria-live="assertive" with a "Slide changed" message, which interrupts and still doesn't say what the slide is.
Try it
Show the second slide with Tab, and check that a screen reader would know it is showing.
In Broken 1, Tab skips the arrows and dots. In Broken 2 they work with the keyboard, but watch the panel below: no dot is ever said to be current, and pressing Next or a dot reads nothing of the new slide.
Broken Can you fix this?
Screen reader says:
(Tab into the carousel, or click a dot)
The fix
Make the arrows and dots named buttons, mark the current dot, and read each new slide out.
- 1Every arrow and dot is a native button with a name.
- 2The current dot is marked with aria-current, and its look is drawn from it.
- 3A slide that a button brings in is read out, without moving focus.
Arrows and dots that take only clicks
<div class="carousel"> <div class="slides"> <div class="slide"> <h2>Paper and light</h2> <p>Our autumn exhibition, until 31 January.</p> <a href="/exhibitions/paper-and-light">See the exhibition</a> </div> <div class="slide" hidden> <h2>Open late on Fridays</h2> <p>Galleries and café open until 9 pm.</p> <a href="/visit">Plan your visit</a> </div> <div class="slide" hidden> <h2>Become a member</h2> <p>Free entry all year, and a first look at every new show.</p> <a href="/membership">Join today</a> </div> </div> <div class="carousel-prev"></div> <div class="carousel-next"></div> <div class="carousel-dots"> <span class="carousel-dot is-current"></span> <span class="carousel-dot"></span> <span class="carousel-dot"></span> </div></div><section class="carousel" aria-roledescription="carousel" aria-label="Highlights"> <button type="button" class="carousel-prev"> <svg aria-hidden="true" focusable="false"><use href="#icon-previous"></use></svg> <span class="visually-hidden">Previous slide</span> </button> <button type="button" class="carousel-next"> <svg aria-hidden="true" focusable="false"><use href="#icon-next"></use></svg> <span class="visually-hidden">Next slide</span> </button> <div class="slides" aria-live="polite"> <div class="slide" role="group" aria-roledescription="slide" aria-label="1 of 3"> <h2>Paper and light</h2> <p>Our autumn exhibition, until 31 January.</p> <a href="/exhibitions/paper-and-light">See the exhibition</a> </div> <div class="slide" role="group" aria-roledescription="slide" aria-label="2 of 3" hidden> <h2>Open late on Fridays</h2> <p>Galleries and café open until 9 pm.</p> <a href="/visit">Plan your visit</a> </div> <div class="slide" role="group" aria-roledescription="slide" aria-label="3 of 3" hidden> <h2>Become a member</h2> <p>Free entry all year, and a first look at every new show.</p> <a href="/membership">Join today</a> </div> </div> <div class="carousel-dots"> <button type="button" class="carousel-dot" aria-current="true"><span class="visually-hidden">Slide 1</span></button> <button type="button" class="carousel-dot"><span class="visually-hidden">Slide 2</span></button> <button type="button" class="carousel-dot"><span class="visually-hidden">Slide 3</span></button> </div></section>Why this fix: Use native HTML first
The arrows and dots respond only to click, with no Tab stop, no name and no role. The current dot is marked by a class that only changes its colour, and a new slide replaces the old one without a word to a screen reader.
Tab now reaches every arrow and dot, and Enter and Space press them, so the keyboard can show every slide. But the screen reader is no better off. The current dot is still marked only by a class, so it reads as "Slide 2, button" whether or not that slide is showing, and the slides sit in a plain div, so pressing Next or a dot changes the slide without a word. A screen reader user can control the carousel and still not know which slide is on screen.
The arrows and dots are real buttons, so Tab reaches them and Enter and Space press them, with no key handling to write, and each has a name. The arrows come before the slides, so the keyboard meets them first. The current dot carries aria-current="true", so a screen reader says "current" on it, and its filled look is drawn from that attribute, so the look and the announcement can't disagree. The polite live region reads the new slide to screen reader users who press an arrow or a dot, without moving their focus, and the slide's label says which of the three it is.
The dots are optional. When the arrows are enough, keep the dots as a picture of where you are, with aria-hidden="true" and no click handler, so there is nothing for the keyboard to miss. When they stay clickable, each one needs everything a button needs.
- a name, here "Slide 1", in hidden text or
aria-label; - a target of at least 24 by 24 pixels, which a 12-pixel dot is not;
- a visible focus ring, and 3:1 contrast against what is behind it;
aria-current="true"on the current one, with its look drawn from that attribute rather than a class.
Keep the announcement short and polite. The live region reads the slide itself, its label and its content, so there is no need for a separate "Slide changed" message. aria-live="assertive" would cut off whatever the screen reader was saying.
aria-roledescription changes what a screen reader calls the region and each slide, so it goes on those two only, and in the page's language: "carousel" and "slide" in English, "カルーセル" and "スライド" in Japanese. The label "1 of 3" tells people where they are; the slide's heading says what it is about.
Swiping can stay, but it is an extra for touch screens, never the only way to change the slide. Many carousel libraries ship an accessibility option, such as Swiper's a11y module; turn it on, then check what it actually does with the steps below. The same applies to product carousels that show several cards at once: the arrows are named buttons, and what they bring into view is read out.
Fixing this with an AI coding assistant? Get this guide as Markdown
Verify the fix
4 checks, no mouse.
Test with: Keyboard, Screen reader, Forced colors
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the carousel, its slides or its library change.
Limits
This is a starting pattern, not a guarantee for every application. It covers how the arrows and dots are reached and what they say. A carousel that also turns by itself needs a way to stop it as well (2.2.2): see tickers, carousels and animations that can't be paused, which also covers stopping the rotation when focus enters a slide. The text over each slide still needs enough contrast against its picture (1.4.3), and a slide's picture needs alt text when it carries information.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
References
- ARIA APG: carousel (opens in a new tab)
- ARIA APG: carousel-1-prev-next (opens in a new tab)
- WAI Tutorial: carousels (opens in a new tab)
- WCAG Understanding: keyboard (opens in a new tab)
- WCAG Understanding: name-role-value (opens in a new tab)
- developer.mozilla.org: aria-live (opens in a new tab)
- developer.mozilla.org: aria-current (opens in a new tab)
Keep this fixed as your code changes.
Turn guides like this into rules your coding agents and CI follow, then have the result retested with a keyboard and screen readers.