Search topics
Star ratings that keyboards and screen readers skip
A review form draws five stars that fill up to the one you click, built from spans with a click handler or from radio buttons hidden with display: none, so Tab never reaches them, a screen reader hears no rating, and the choice exists only as filled stars.
<span class="star" onclick="rate(4)"> The problem
Keyboard and switch users can't choose a rating, and screen reader users hear neither the stars nor which one is chosen, so they can't rate a product or check the rating they gave.
A shop's review form is drawn with a row of five large stars under "Your rating": grey outlines that fill amber up to the one you click. Stars are what every shopper already knows how to read, and a row of them asks for an opinion more lightly than a dropdown or a row of numbers would. The developer builds what was drawn, in one of two common ways. Either each star is a span with a click handler that fills it and the stars before it, or each star is the label of a radio button, with the radio buttons hidden with display: none and the stars filled from the one that is checked. With a mouse, both look the same and work the same: click the fourth star, and four stars fill.
Put the mouse aside and press Tab from the question. Focus goes straight to Post review: no star is ever reached, and no key fills one. A screen reader reads "Your rating", then "Post review, button". With the spans, the stars are silent. With the hidden radio buttons, it reads the words hidden in the labels, "1 star" to "5 stars", as five lines of plain text, with no radio buttons and nothing to say which one is chosen. Click the fourth star with a mouse and listen again: what is read hasn't changed. The rating exists only as four filled stars.
The stars are drawn as controls but aren't built as any. A span is not focusable and has no role, and a click handler adds neither. A radio button with display: none is taken out of the page entirely, out of the Tab order and out of the accessibility tree, while a click on its label still checks it, so the mouse path keeps working and hides the problem from anyone testing with a mouse.
That fails three criteria. 2.1.1 Keyboard: a click chooses a rating, but no key can, because nothing is left to focus. 4.1.2 Name, Role, Value: what people see is five choices of one value with one chosen, but software finds no radio button, no name such as "4 stars", and no checked state, so a screen reader can't say what the stars are or which is set. 1.3.1 Info and Relationships: the drawing shows three things at once: that "Your rating" is the question the stars answer, that the five stars belong together as one choice, and how many are chosen. In the code, with the spans, the question is a paragraph tied to nothing, the stars are separate spans, and the choice is a class on four of them. With the hidden radio buttons, the fieldset still groups the stars under their question, but the radio buttons that make them one choice, and say which is chosen, have been removed. Either way, the choice exists only on screen. This is failure F15 in WCAG's own list: a custom control that doesn't expose itself through the accessibility API.
Ratings come at the end of something: a review after a purchase, "How did we do?" after a support chat, a score for a course or a recipe. When only a mouse can set one, the review goes without its rating, and the ratings a shop collects come only from people who can click or tap.
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 Keyboard and switch users have nothing to reach: Tab goes from the question straight to Post review, and no key fills a star, so their review goes without a rating, or can't be sent when a rating is required.
- Without vision Screen readers find no control and no value: the spans are silent, and with the radio buttons removed, the hidden words "1 star" to "5 stars" are read as plain text, with nothing to say which one is chosen.
Most of what is wrong is what's missing. axe tests what is rendered, so it skips a radio button with display: none, and an empty span with a background star has no role to check. A scan of what is there finds nothing wrong. EaseWeb's audit finds both forms: a row of stars in an element whose class or id calls it a rating or stars, each with a pointer cursor and none reached by Tab, as ew:rating-star-unreachable, and a radio button hidden while its label is shown, including star labels with no words, as ew:hidden-native-input. When a rebuild gives the stars role="radio" without a name, axe reports aria-toggle-field-name. When asked to fix it, AI coding tools can add tabindex="0" to each span: Tab then stops five times, but no key chooses, and nothing announces a radio button or which one is chosen. Another patch adds aria-label="4 stars" to each span, which a span can't carry, and still leaves the keyboard out. Put the mouse aside and try to rate.
Try it
Click a star, then put the mouse aside: press Tab to reach the stars and give 4 stars with the arrow keys.
With a mouse, both versions fill the stars. The panel shows what a screen reader hears as focus moves and the choice changes: is a star ever announced, or which one is chosen? In Broken 2, the stars are real radio buttons, removed with display: none.
Broken Can you fix this?
Screen reader says:
(Tab through the form)
The fix
Build the stars as one radio group named by its question, with each star named by its value.
- 1Build the stars on radio buttons, or on role="radio" with aria-checked, in one group named by the question.
- 2Name each star by its value, "1 star" to "5 stars".
- 3Draw the filled stars and the focus ring from the checked and focus states.
Five spans with onclick
<form action="/reviews" method="post"> <p class="question">Your rating</p> <div class="stars"> <span class="star" onclick="rate(1)"></span> <span class="star" onclick="rate(2)"></span> <span class="star" onclick="rate(3)"></span> <span class="star" onclick="rate(4)"></span> <span class="star" onclick="rate(5)"></span> </div> <input type="hidden" name="rating"> … <button type="submit">Post review</button></form>Radio buttons over the drawn stars
<form action="/reviews" method="post"> <fieldset class="stars"> <legend class="question">Your rating</legend> <label class="star"> <input type="radio" name="rating" value="1"> <span class="visually-hidden">1 star</span> </label> <label class="star"> <input type="radio" name="rating" value="2"> <span class="visually-hidden">2 stars</span> </label> <label class="star"> <input type="radio" name="rating" value="3"> <span class="visually-hidden">3 stars</span> </label> <label class="star"> <input type="radio" name="rating" value="4"> <span class="visually-hidden">4 stars</span> </label> <label class="star"> <input type="radio" name="rating" value="5"> <span class="visually-hidden">5 stars</span> </label> </fieldset> … <button type="submit">Post review</button></form>Why this fix: Use native HTML first
The spans take a click and nothing else. They aren't in the Tab order, they have no role, name or state, and the rating lives in a class and a hidden form value that no assistive technology reads.
The design is kept exactly as drawn: the same stars, the same fill, nothing new on screen but a focus ring. The radio buttons are made transparent instead of removed, each one over its own star, so they stay in the Tab order and the accessibility tree, and a touch on a star lands on its radio button. The browser does the rest. Tab stops on the rating once, the arrow keys move the choice one star at a time, and a screen reader announces the question, "Your rating", then something like "4 stars, radio button, checked, 4 of 5". The question is the group's legend, and the stars fill from the checked radio button itself, so what is drawn can't drift from what is announced. The radio buttons sit in source order, 1 to 5, and :has() fills the stars before the checked one, so nothing is reversed to make the fill work.
All three pass. Radio buttons over the drawn stars keep the design exactly as it is, with no script, and leave the keys, the focus, the form value and the touch position to the browser; use them whenever the stars can be labels. The ARIA radio group keeps elements a script or a component draws, at the cost of writing and testing the keys, the focus and the states yourself; use it only when the stars can't be labels. Writing the rating in words beside the stars is the designer's choice: it adds a line to the form and gives the rating to people who can't, or would rather not, count stars. Decide it in the design file, where there is room for the words on a phone as well as on a desktop.
The classic CSS-only star rating writes the radio buttons from 5 down to 1 and turns the row round with flex-direction: row-reverse, so that input:checked ~ label can fill the stars after the checked one, which then appear before it. Don't copy it: a screen reader then reads "5 stars" first, and the arrow keys move the choice the opposite way to the arrow pressed. :has(), as in the fixes, fills the stars before the checked one with the source in order.
Radio buttons can't be unchecked. A rating that can be left empty needs a way back, such as a "Clear rating" button after the stars, which unchecks the group in a script and puts focus back on the first star. A rating that must be given can carry required on its radio buttons: the browser then moves focus to the group and says what is missing when the form is sent.
Many designs preview the rating as the pointer moves along the stars. A preview drawn on :hover alone does no harm when the radio buttons are there, because the arrow keys check each star as they move, so a keyboard user sees the real choice instead of a preview. Half stars need a radio button for each half, ten in all, named "half a star" to "5 stars", and halves drawn on each star; a script that reads which half of a star the pointer is on gives the keyboard no way to choose them.
In a Windows contrast theme, the browser replaces colours with the user's own, but keeps background images, so the stars keep their amber and grey there, and an outlined star can still be told from a solid one. The focus ring is drawn with outline, which the browser redraws in the theme's colour. Stars drawn as an SVG fill in a fixed colour can vanish against a dark theme: draw them with currentColor, as the W3C example does.
Checkboxes and radio buttons hidden in other ways, and the other fixes for them, such as drawing the box on the input with appearance: none, are covered in custom checkboxes that keyboards and screen readers skip. A rating shown after it is given, such as a product's average beside its name, is not a control: it needs the rating in words, "4 out of 5", as aria-label on divs and spans that screen readers often skip shows.
Fixing this with an AI coding assistant? Get this guide as Markdown, or get every guide in your coding agent.
Design ideas
A rating can be stars, numbers or faces, as long as a key can choose it.
Four ways to draw a rating. Click them, then put the mouse aside and try each with Tab and the arrow keys: three pass, and one gives the mouse a half star that no key can give.
In the design file
- Mark the stars in the design file as one question with five choices, a radio group, so they are built on radio buttons.
- Write the question above the stars, and a name for each star, “1 star” to “5 stars”, which a screen reader reads out.
- Draw a star with its focus ring, as it looks when reached with Tab and moved with the arrow keys.
- Draw the rating before anything is chosen, and say whether it can be cleared once chosen.
Passes
- Numbered circles, the ends named Five 40px circles numbered 1 to 5, with “Poor” and “Excellent” under the ends, as survey tools draw them. The number is the rating, so it reads at a glance and in greyscale; the chosen circle fills with #1a1a1a ink, 17.40:1 on white.
- Faces, the chosen one named Five faces from very unhappy to very happy, each named for what it means, and the chosen one’s word written under the row, so nobody has to read a face. The chosen face takes an amber tint and a #b45309 ring, 5.02:1 on white.
- Stars with a quiet Clear A rating that may stay empty needs a way back, since a chosen star can’t be unchosen by a key. “Clear”, underlined in #4b5563, 7.56:1, appears after the stars once one is chosen, and empties them for the mouse and the keyboard alike.
Fails
- Half stars set by where the pointer landsWCAG 2.1.1 A click on the left half of the fourth star gives 3.5, but Tab and the arrow keys move only between the five whole stars, so no key gives a half. Draw ten choices if halves matter, or keep whole stars.
Verify the fix
5 checks.
Test with: Keyboard, Screen reader, Mouse, Forced colors, Touch
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the rating component or the theme changes.
Limits
This is a starting pattern, not a guarantee for every application. The CSS is a sketch: check the size of the stars, their contrast and the focus ring against your own palette. Stars told apart only by colour, such as a grey star and a yellow star of the same shape, are a different failure, left to the topic on colour alone, whose guide is in progress. A group of radio buttons whose question isn't tied to them is covered in radio buttons and checkboxes without a group name. Test the whole review form with a keyboard, a screen reader and a phone.
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
- WCAG Understanding: keyboard (opens in a new tab)
- WCAG Understanding: name-role-value (opens in a new tab)
- WCAG Understanding: info-and-relationships (opens in a new tab)
- WCAG Technique F15 (opens in a new tab)
- ARIA APG: radio-rating (opens in a new tab)
- ARIA APG: radio (opens in a new tab)
- developer.mozilla.org: :has (opens in a new tab)
- developer.mozilla.org: fieldset (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.