Search topics
Select-all checkboxes whose dash is never read out
A select-all checkbox shows a dash when only some rows are ticked, but the dash comes from a class, so screen readers still announce it as not checked.
all.classList.toggle('partial', ticked > 0 && !all.checked); The problem
Screen reader users hear the select-all as not checked while some rows are ticked, so they can't tell from it that anything is selected.
An account page lists invoices, each with a checkbox, under a Select all checkbox and above a Download selected button. Tick March and April, and the Select all box changes from empty to a dash. The dash is the list's summary: at a glance, it says that some invoices are ticked, but not all. On a long list it is often the only sign that a row further down, out of view, is still ticked.
With a screen reader, tick the same two invoices, then press Shift + Tab to go back up to Select all. It is announced as "Select all, checkbox, not checked". The box on screen shows a dash; the checkbox in code is simply not checked. Press Space and every invoice is ticked, as it would be with a mouse, so the box works. What fails is what it says: a screen reader user who comes to the list later, or reads it from the top, hears that Select all is not checked and has no way to tell, short of reading every row, that two invoices are already selected.
Success criterion 4.1.2 Name, Role, Value asks that the state of a control people can set is available to assistive technology. A checkbox shown with a dash is in a third state, partly checked, and that state is only on screen.
An HTML checkbox has two checked values, checked and not checked. The third look comes from a separate flag, indeterminate, and it exists only as a property in script: there is no indeterminate attribute, so it can't be written into the markup or set by a template as one. When a script sets it to true, the browser draws its own dash, matches :indeterminate in CSS, and reports the checkbox as mixed, which screen readers read as "partially checked", "half checked" or "mixed". The script here never sets it. It adds a class, and the CSS draws a dash on the box from the class, so the drawing and the state the browser reports are two separate things, and only the drawing changed.
The same dash is often wired up in other ways that fail the same way:
aria-checked="mixed"added to the native checkbox. ARIA in HTML forbidsaria-checkedon aninput type="checkbox", and Chrome ignores it and reports the input's own checked state, so the box still reads as not checked.- A custom checkbox, a
buttonordivwithrole="checkbox", whose script setsaria-checkedonly to"true"or"false", never to"mixed". - A script that sets
checked = trueas soon as any row is ticked, then draws a dash over the tick: the box shows a dash and is read as checked, as if every invoice were selected.
Select all sits above lists that run past the screen: invoices, messages, files, orders, rows in a table. Its look is the one place that sums up the whole list, and when the summary is only drawn, people who don't see it get no summary at all.
1 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 (not affected)
- Reach, strength (not affected)
- Cognitive (not affected)
- Seizures (not affected)
- Without vision Screen reader users reach Select all and hear "not checked" while two invoices are ticked. The dash that tells a sighted person some are selected is never read, so to find out, they have to move through every row.
The checkbox has a label, a role and a valid state, so axe and other scanners pass it: nothing in the markup is wrong, only missing, and a scanner doesn't compare what is drawn with what is reported. EaseWeb's audit ticks one row with Space and reports a select-all that is redrawn while it still reads as not checked, as ew:select-all-state-missing. Asked to fix it, AI coding tools can add aria-checked="mixed" to the native checkbox, or write indeterminate into the markup as an attribute. Neither changes what Chrome reports: the checkbox still reads as not checked. axe does catch the first, as aria-conditional-attr, since aria-checked on a native checkbox disagrees with its real state, but the fix it then asks for is to remove the attribute, which leaves the dash unreported again. Check the select-all's state in the browser's accessibility panel, not its class.
Try it
Tick two invoices, then go back to Select all and listen to how it is read.
Tab to March and press Space, then Tab to April and press Space, and press Shift + Tab twice. The panel shows what a screen reader says as focus moves: compare what it says on Select all with the box you see, then press Space there.
Broken Can you fix this?
Screen reader says:
(Tab through the list)
The fix
Give the select-all a mixed state in code, and draw the dash from it.
- 1Set a mixed state whenever some rows but not all are ticked: the indeterminate property, or aria-checked="mixed".
- 2Draw the dash from that state, never from a class, and as a mark, not by colour alone.
- 3Never put aria-checked on a native checkbox.
<form action="/invoices/download" method="post"> <label class="check"> <input type="checkbox" id="select-all"> Select all </label> <ul class="invoices"> <li> <label class="check"> <input type="checkbox" name="invoice" value="2026-03"> March 2026 </label> </li> … </ul> <button type="submit">Download selected</button></form>The indeterminate property
<form action="/invoices/download" method="post"> <label class="check"> <input type="checkbox" id="select-all"> Select all </label> <ul class="invoices"> <li> <label class="check"> <input type="checkbox" name="invoice" value="2026-03"> March 2026 </label> </li> … </ul> <button type="submit">Download selected</button></form>Why this fix: Use native HTML first
With two of the rows ticked, the script sets checked to false and adds the partial class, and the CSS draws the dash from the class. The browser reports what the checkbox holds, not what is drawn on it, so a screen reader announces "not checked".
The script sets the checkbox's own indeterminate property where it used to add the class, and the CSS draws the dash from :indeterminate, which matches exactly while the property is true. The state a screen reader reads and the dash people see now come from one value, so they can't disagree. The remove('partial') line goes too: when someone presses a mixed checkbox, the browser clears indeterminate itself and ticks the box, so Select all then ticks every row, as it did before.
Both pass. Use the indeterminate property whenever the select-all is a native checkbox, which it can nearly always be: it is one line in the script you already have, the browser reports the mixed state and clears it when the box is pressed, and nothing else has to change. That is why it is the recommended fix. Use aria-checked="mixed" when the select-all is already a custom checkbox, such as one from a component library that draws every checkbox as a button, and set all three states from the same script that ticks the rows. Don't rebuild a native checkbox as a custom one to get the dash: the property gives it the dash and the mixed state as it is.
Because indeterminate is only a property, nothing in the markup can set it, and nothing restores it. Set it when the page loads, from the rows that are already ticked, and again whenever the rows change without anyone ticking one: after a form reset, which restores each checkbox's checked state but leaves indeterminate alone, after filtering or paging the list, and after rows are added or removed. A template or framework that writes indeterminate into the markup does nothing: set the property in the component's script, for example from a ref after each render.
Pressing a mixed checkbox ticks it here: Select all goes from a dash to a tick and ticks every row. The checkbox pattern in the ARIA Authoring Practices also shows a select-all that ticks all, then unticks all, and on the third press returns to the partial selection someone had made. That takes more script, and either behaviour passes 4.1.2, as long as the state reported matches the box at every step.
Draw "some" as a mark of its own, a dash or a small square, not as a paler fill of the box: a fill alone tells some from none by colour only, which fails 1.4.1 Use of Color. Draw the dash with a border, as here, not with a filled shape. In a Windows contrast theme the browser removes background fills but redraws borders in the user's colours, so a dash drawn as a border stays, and the three states can still be told apart.
Nothing needs announcing when a row's tick changes Select all. Focus is on the row, and a screen reader reads Select all's state when someone moves to it, which is when they need it. A live region saying "2 of 4 selected" can be added as a convenience, but it doesn't replace the mixed state.
Design ideas
None, some, all: three states, and a mark for each.
Four ways sites draw a select-all over a list. Tick and untick the rows with the mouse, or with Tab and Space: three show “some” with a mark or words of their own, and one only with a paler fill.
In the design file
- Draw the select-all empty, with a tick when every row is chosen, and with its own mark when only some are.
- Make the “some” mark a shape, such as a dash or a small square, never only a paler fill, and check the three states in greyscale.
- Write the ratio of each mark against what it sits on, at least 3:1. The example’s violet dash on white is 6.29:1.
- Note on the select-all that its middle state is the checkbox’s own, set in code, not a style of its own.
- Draw the three states in a Windows contrast theme, where fills disappear and every mark must still show.
Passes
- A small square inside the box Some chosen, the box keeps its white fill and #6b7280 edge, 4.83:1, and holds a small violet square, 6.29:1 on the white; all chosen, it fills violet with a tick. Three shapes, so the states read in greyscale, and the square is drawn as a border, which a contrast theme keeps.
- A dash in an outlined box A lighter look than a filled box: some chosen, the edge turns violet and a violet dash sits on the white, 6.29:1; only all chosen fills the box. The dash, not the colour, tells some from none, and it is drawn as a border, so it stays in a contrast theme.
- A selection bar that counts Once a row is chosen, the row above the list becomes a #eef2ff bar: the box with its dash, “2 of 3 selected” in #3730a3, 8.88:1 on the tint, and a Clear button. The count says how many in words, and Clear undoes the choice in one press.
Fails
- “Some” shown by a paler fillWCAG 1.4.1 All chosen is a violet box with a tick; some chosen is the same box in a pale #c7d2fe with no mark, 1.49:1 against the white of an empty one. The checkbox does report “mixed”, but on screen, in greyscale or in a contrast theme, where the fill disappears, some and none look the same.
Verify the fix
4 checks, no mouse.
Test with: Screen reader, Keyboard, Forced colors
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the list component or its checkboxes change.
Limits
This is a starting pattern, not a guarantee for every list. It covers the select-all's state; a checkbox that the keyboard can't reach at all is covered in custom checkboxes that keyboards and screen readers skip, and a list whose chosen options are never read out in multi-selects with silent ticks and mouse-only chips. A tree of checkboxes, where each group has its own select-all, needs the same mixed state at every level. Test the whole list with the screen readers your users rely on.
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
EaseWeb service
Fixed it? Keep it from coming back.
I turn guides like this one into rules your coding agents follow and checks your CI runs. Then I retest by hand with a keyboard and screen readers, periodically.