Search topics
Filter links that don't say which is chosen or what changed
Category links above a product list redraw it in place and fill the chosen pill by a class, so a screen reader hears no change and no chosen filter.
link.classList.add('is-active'); The problem
Screen reader users press a filter and hear nothing, so they can't tell that the list changed or which filter is now chosen.
A shop's tableware page lists six products under a row of pill-shaped links: All, Mugs, Plates and Bowls. Each pill is a real link with its own address, so it can be opened in a new tab or shared. A script stops the page from loading: pressing Mugs fetches the mugs, redraws the list below, changes the address to ?category=mugs, and fills the Mugs pill in black. A sighted visitor sees the pill fill and the list shrink to two mugs. A screen reader user hears "Mugs, link", presses Enter, and hears nothing. No new page loads, so there is no new page title to read, and the script says nothing either. Moving back onto the pill, they hear "Mugs, link" again, exactly as before, and the same for every other pill. They can't tell that the list changed, or which filter is in force, without reading down through the products.
Each pill is a control people use to choose a filter, and the filled one shows a state: this is the filter in force. Success Criterion 4.1.2 Name, Role, Value asks that states people can set are available to assistive technology, and that changes to them are reported to it. Here the state is a class, is-active, that only the stylesheet reads, so assistive technology has no state to report before the press or after it. The script also does what a page load did, swapping the content and the address, but leaves out what a page load gives for free: something that is read. The link says it will take people somewhere, and when it changes the page in place instead, nothing comes back to say where they are now.
Filter rows like this sit above product grids, article lists and search results. On a phone they are often one strip that scrolls sideways, with the list that changes below the edge of the screen, so anyone looking at the strip sees only the pill fill.
2 of 10 groups affected
- No vision (affected)
- Low vision (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 A screen reader says "Mugs, link" before the press and after it, and nothing in between, so the person can't tell whether the list changed or which filter is in force, and reads down through the products to find out.
- Limited vision Someone zoomed in on the filter row doesn't see the list change below it, and in a Windows contrast theme the pill's fill, the only mark of the choice, is replaced with the page colour.
A scan reads four valid links with names and addresses, and passes them. It doesn't press them, so it never sees the page change in place, and a class called is-active, selected or current means nothing to it. EaseWeb's audit reports the chosen pill when its address is the page being shown and it looks unlike the other links in its row, but nothing in its code marks it as chosen. It sends each row of filter links to review, since only a press shows what is said. AI assistants asked to fix filter links can reach for aria-selected or role="tab", which belong to tabs and listboxes. Some turn the links into buttons, which loses the addresses people open in new tabs and share. Others put aria-live on the product list, which reads out every product after each press, or nothing when the list is replaced.
Try it
Choose a filter and listen for what is said.
Tab to Mugs and press Enter. The panel shows what a screen reader says. After each press, Tab along the filters again: is the chosen one ever called current, and is the change ever heard?
The fix
Mark the chosen filter, and say what the page now shows.
<title>Mugs | Tableware</title>…<ul class="filters"> <li> <a href="/shop">All</a> </li> <li> <a href="/shop?category=mugs" class="is-active">Mugs</a> </li> <li> <a href="/shop?category=plates">Plates</a> </li> <li> <a href="/shop?category=bowls">Bowls</a> </li></ul><ul id="products">…</ul>What is wrong2 points
- The
is-activeclass fills the Mugs pill and does nothing else: a screen reader reads "Mugs, link", the same as the other three. - The script stops the page load and redraws the list in place, so nothing is read after the press: no new page title, and no message.
Mark the chosen link, and say what changed
<title>Mugs | Tableware</title>…<ul class="filters"> <li> <a href="/shop">All</a> </li> <li> <a href="/shop?category=mugs" aria-current="page">Mugs</a> </li> <li> <a href="/shop?category=plates">Plates</a> </li> <li> <a href="/shop?category=bowls">Bowls</a> </li></ul><p id="filter-status" role="status" class="visually-hidden"></p><ul id="products">…</ul>How it is fixed5 steps
- Replace the class with
aria-current="page"in the markup the server sends. The link's address is the page being shown, and a screen reader reads it as the current page, with something like "Mugs, current page, link". - Draw the filled look from
[aria-current="page"], so the pill that looks chosen and the one that is announced are always the same. - Put an empty
role="status"region under the filter row, hidden visually. It is in the page from the start and outside the list that is redrawn, so writing into it is a change the browser reports. - Once the new list is in, move
aria-currentto the pressed link and write "Now showing: Mugs" into the region. The screen reader finishes what it is saying, then reads it, and focus stays on the pill, ready for the next filter. - Do nothing when the pressed link is already the current one, so the list isn't fetched again for the filter in force.
This fixes the Before and passes 4.1.2: the chosen filter is marked in code, and the change is reported as it happens. The mark is what 4.1.2 asks for, but whether a screen reader speaks a change to aria-current on the link that has focus is not something tested here, so the message says it in words.
Why this fix: Style from the state, not a class
Both mark the chosen filter and tell people what changed. The update in place keeps the visitor's place, with focus on the filter row, so they can try Plates straight after Mugs; it costs a status region and a handler that has to keep the mark, the message and the list in step. The page load needs no script for the update and nothing to keep in step, because the browser and the screen reader do what they do for any link. It costs a page load each time, and focus and scrolling start again at the top. Let the link load its page when the filters are rarely pressed or the update script is there only for speed; keep the update in place when people try several filters in a row.
A link or a button. A filter with its own address, one people can open in a new tab, bookmark or share, is a link and stays one. A filter that changes the list with no address of its own is a button: give it aria-pressed, as in toggle buttons that don't say whether they are pressed. See buttons announced as links that Space can't press for how the two roles differ.
Which value of aria-current. Use page: the link's address is the page being shown, and the filter row is a set of pages. aria-current="true" marks the current item in a set too, and a screen reader reads it as "current". Don't use aria-selected, which belongs to tabs, options, and the rows and cells of a grid, or role="tab", which would make the row a tab list with keys of its own.
Say what is shown, not the whole list. The message names the filter in force. To say how many products are left as well, put the count in the same message, written once per press; see search and filter results that update without a word. Never put aria-live on the product list.
Keep the region out of what is redrawn. Render the status region in the page layout, or in a part of the filter component that stays mounted, never inside the list or a block that is replaced along with it. A region created along with its message is often not announced.
Sort links. A row such as "Sort: Newest · Price, low to high · Price, high to low" is the same pattern: aria-current="page" on the link for the order in force, and in place, a message such as "Now sorted by price, low to high". Sorting by a table's column header is a different pattern, with aria-sort on the header, covered by the sortable tables topic.
What a link that stops itself must keep. A handler that calls preventDefault() on every click also stops Ctrl and Cmd clicks, so the filter no longer opens in a new tab. Leave clicks with a modifier key to the browser. Redraw the list on popstate, too, so Back returns to the earlier filter with the mark moved to match. Neither is a 4.1.2 failure, but both are what people lose when a link stops acting like one.
Contrast themes. A Windows contrast theme replaces the black fill with the page colour, so the chosen pill looks like the rest. Give it a mark the theme keeps, such as a thicker border, or fill it with Highlight and HighlightText under @media (forced-colors: active); see high contrast mode hiding focus, icons and selections.
Design ideas
A chosen filter can take many looks and still be heard.
Four more ways to draw the filter row, in the guide’s own ink. Click a filter to move the choice, and Tab along the rows: three mark the choice where a screen reader finds it, and one only shows the change by moving.
In the design file
- Draw the chosen filter as its own state, marked by a tick, a bar or bold text as well as any fill, because a Windows contrast theme removes fills.
- Draw hover and focus on every filter, the chosen one included, since each stays a link.
- Write beside the row the words the page says after each press, even when they are only read aloud.
Passes
- Chip with a tick The chosen chip gains a tick and bold text in #1d1d1f, 14.39:1 on its pale #e6eef8 fill. The fill is only 1.17:1 against the page, so it is decoration; the tick and the weight carry the choice, and both stay in a contrast theme.
- Underlined row, like tabs The filters sit on a hairline, the chosen one bold with a 3-pixel bar under it in #1d1d1f, 16.83:1 on white. The others are #5f6368, 6.05:1, but only 2.78:1 from the chosen ink, so the bar and the weight do the work. It looks like tabs and stays a row of links, with no arrow keys to learn.
- A “Now showing” line under the pills The guide’s pills, with a line under them in #5f6368, 6.05:1, that changes to “Now showing: Mugs” on each press. Sighted visitors on a phone see the change without scrolling to the list, and the same line is what a screen reader hears.
Rule of thumb: Show a message next to what it is about
Fails
- Grid that shuffles into placeWCAG 4.1.2 The tiles glide to their new places in 0.4 seconds, a movement that shows the change and stops. But it is the only report: the filled pill is drawn from a class, and nothing is said, so a screen reader hears “Mugs, link” and no change at all.
Rule of thumb: Move to show a change, then stop
Verify the fix
5 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 filter component, the list template or the page layout changes.
How screen readers handle role="status" and aria-current="page"
Limits
Live regions behave differently across screen readers and browsers, and a polite message can be dropped if people keep pressing. This guide covers one row of links that each choose one filter. Filters people combine, several ticked at once, are usually checkboxes, and announcing their results is covered in search and filter results that update without a word. A link that replaces the whole view of a single-page app is covered in single-page app route changes that are never announced, and the current page in a site's menu in navigation that marks the current page only by its look. Test your real filters, with their sorting and pagination, with the screen readers your users use.
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.
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.