Search topics
Tree views keyboards can't open and screen readers flatten
A sidebar of sections and pages is built from clickable divs, or given tree roles without the keys, states and levels they promise, so the keyboard can't open its sections and a screen reader hears every item at the same level.
<div class="section">Account</div> The problem
Keyboard users can't open a sidebar's sections to reach the pages inside them, and screen reader users can't tell which section a page is in, how deep it sits, or whether a section is open.
An app's help centre has a sidebar of topics in three sections: Getting started, Billing and Account. Click a section and its pages unfold beneath it, indented, and its arrow turns down. Inside Billing, Payment methods is a section of its own, holding Cards and Bank transfer one level deeper. The sections are divs with click handlers. Put the mouse aside and press Tab: focus moves through the links that happen to be showing, Plans and prices and Invoices, then leaves the sidebar for Contact support. Getting started, Payment methods and Account can't be opened, so the pages inside them can't be reached. A screen reader hears the visible links one after another, with nothing to say they belong to Billing, and the section names as plain text with no sign that they open.
The team then makes it a tree the quick way: role="tree" on the sidebar, role="treeitem" on every section and link, and nothing else. For the keyboard, nothing changes: Tab still passes over every section. A screen reader now calls the sidebar a tree, but the arrow keys do nothing in it, every item is announced at level 1, "Invoices, tree item, level 1", and no section says whether it is open.
Success criterion 2.1.1 Keyboard asks that everything the mouse can do, the keyboard can do. Opening a section is done only by clicking a div, which takes no focus, so the pages inside a closed section can't be reached without a mouse. Adding the tree roles changes nothing here: the sections still take no focus, and the arrow keys a tree promises do nothing.
Success criterion 4.1.2 Name, Role, Value asks that controls expose their role and state. The div sections expose neither: no role that says they can be pressed, and no expanded or collapsed state. The tree's sections have the role but still no aria-expanded, so a screen reader can't tell an open section from a closed one, or a section from a page.
Success criterion 1.3.1 Info and Relationships asks that structure shown on screen is in the code. The indentation shows that Invoices belongs to Billing and that Cards is a level deeper. In the divs, nothing in the markup says so. With the tree roles, browsers work out each item's level from the nesting of group elements, and with none there, every item is level 1. The roles make the wrong structure official.
Help centres, documentation and account settings often put every page behind a sidebar like this, and a narrow screen usually moves the same sidebar into a menu, where the failure follows it. A section that can't be opened hides its pages from anyone who can't click it.
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 can't open a section built from a div, so every page inside a closed section is out of reach. Adding tree roles to the same divs changes nothing for them: the sections still take no focus, and the arrow keys do nothing.
- Without vision Screen reader users hear the pages as one flat run of links or tree items, all at level 1, with no word of which section each is in or whether a section is open, so they can't tell where they are or that closed sections hold more pages.
Divs with click handlers have no role, so axe has nothing to check in the first sidebar. The tree roles pass axe: every treeitem sits in a tree, and every one has a name. axe checks the shape of a tree's roles (a treeitem outside a tree or group is reported as aria-required-parent, and a tree with no items is sent for review under aria-required-children), never whether a key does anything or whether the levels match the indentation.
The audit presses the keys. In each tree whose items share one Tab stop, it focuses that item and presses the arrow keys; if focus never moves, the other items can't be reached, and that is reported. A section treeitem whose group is hidden but which has no aria-expanded is reported too. It sends to review a tree where more than one item is a Tab stop and the arrows do nothing, a tree with no group and no aria-level whose items are indented on screen, and a container named as a tree, built from divs, with clickable rows Tab never reaches. A div sidebar with no telling class names is not found, and no check can tell whether the levels make sense to a listener.
The second sidebar is what an AI fix can produce: it adds role="tree" and role="treeitem" to the existing divs and stops there, and the result passes axe. A fix can also put tabindex="0" on every row, or add aria-level but no aria-expanded.
Try it
Open Account and reach Change your email using only the keyboard.
- Broken 1: press Tab. It stops only on the links already showing, never on a section, so Account can't be opened.
- Broken 2: the same divs, with tree roles added. Tab still skips every section, the arrow keys do nothing, and the panel says every item is level 1.
- Apply the fix. In the tree view, the arrow keys move and open sections, and Home and End jump to the ends.
Broken Can you fix this?
Screen reader says:
(Tab into the sidebar)
The fix
Make every section a control that opens from the keyboard and says whether it is open, and put its pages inside it in the markup.
- 1Every section opens from the keyboard and says whether it is open.
- 2A page's section and depth are in the markup, not only in its indentation.
- 3A role="tree" gets everything the role promises, one Tab stop, the arrow keys, levels and states; without them, use lists and buttons.
Sections built from clickable divs
<div class="help-nav"> <div class="section">Getting started</div> <div class="pages" hidden> <a href="/help/create-account">Create an account</a> <a href="/help/invite-your-team">Invite your team</a> </div> <div class="section is-open">Billing</div> <div class="pages"> <a href="/help/plans">Plans and prices</a> <div class="section">Payment methods</div> <div class="pages" hidden> <a href="/help/cards">Cards</a> <a href="/help/bank-transfer">Bank transfer</a> </div> <a href="/help/invoices">Invoices</a> </div> <div class="section">Account</div> <div class="pages" hidden> <a href="/help/change-email">Change your email</a> <a href="/help/delete-account">Delete your account</a> </div></div>Nested lists with a button for each section
<nav class="help-nav" aria-label="Help topics"> <ul> <li> <button type="button" aria-expanded="false" aria-controls="help-start">Getting started</button> <ul id="help-start" hidden> <li><a href="/help/create-account">Create an account</a></li> <li><a href="/help/invite-your-team">Invite your team</a></li> </ul> </li> <li> <button type="button" aria-expanded="true" aria-controls="help-billing">Billing</button> <ul id="help-billing"> <li><a href="/help/plans">Plans and prices</a></li> <li> <button type="button" aria-expanded="false" aria-controls="help-payment">Payment methods</button> <ul id="help-payment" hidden> <li><a href="/help/cards">Cards</a></li> <li><a href="/help/bank-transfer">Bank transfer</a></li> </ul> </li> <li><a href="/help/invoices">Invoices</a></li> </ul> </li> <li> <button type="button" aria-expanded="false" aria-controls="help-account">Account</button> <ul id="help-account" hidden> <li><a href="/help/change-email">Change your email</a></li> <li><a href="/help/delete-account">Delete your account</a></li> </ul> </li> </ul></nav>Why this fix: Use native HTML first
The sections respond only to click and take no focus, so a closed section's pages are out of reach for the keyboard. Nothing in the markup says a section can be opened, whether it is, or that the links after it belong to it: the nesting is padding.
Each section is a native button, so Tab reaches it and Enter and Space open it, with no key handling to write, and aria-expanded makes a screen reader say "Billing, button, expanded" or "collapsed". Each section's pages are a list nested inside its list item, so the structure is in the markup: screen readers announce a list and how many items it has as you move into it, and some also say how deeply it is nested. hidden keeps a closed section's links out of the Tab order, and the open section's list follows its button, so the next Tab goes straight into it. This is the disclosure pattern that the ARIA Authoring Practices advise for navigation with expandable groups of links.
Both pass. For a sidebar of pages, like this one, use the nested lists: they are links and buttons that every browser, screen reader and voice control program already knows, need a few lines of script, and leave people to move with Tab as they do on the rest of the page. The ARIA Authoring Practices say the same: their own tree navigation example notes that the disclosure pattern suits typical site navigation better, because few sites need the keyboard behaviour the tree role requires. Build the tree when the sidebar is a widget in an application, where people pick or act on items rather than go to pages, such as a folder picker or a file manager, and when there are too many items to tab through one by one. Then it needs all of it: the roles, the states, one Tab stop and every key.
Mark the page that is open. In the lists, put aria-current="page" on its link, as navigation that marks the current page only by its look explains; in the tree, put it on its treeitem, and open the sections above it on load, so the current page is in view and is where Tab lands. aria-selected is for a tree where choosing an item is a step apart from going to it, such as picking a folder before pressing Move; a tree of links doesn't need it. In a tree where several items can be chosen, the ARIA Authoring Practices advise aria-checked instead.
The browser can only count levels from the nesting it can see. A tree that is drawn flat, or only partly, because it is long, virtualised or loads its branches on demand, needs aria-level, aria-setsize and aria-posinset on every item. The Authoring Practices also suggest declaring them if testing shows a browser and screen reader reporting the wrong values.
The tree view pattern also recommends type-ahead, where a letter moves to the next item that starts with it: for every tree, and especially one with more than seven items at the top level. This sidebar has three. The asterisk, which opens every section at the same level as the focused one, is optional. Right on a closed section opens it without moving focus, as the pattern requires, so people stay where they are; the next Right goes in.
Don't put links or buttons inside a treeitem: the item is the control, and a control inside it is a second Tab stop with nothing to say where it belongs. In the lists, the buttons and links are separate on purpose. On a narrow screen, the same lists or tree go into the menu panel; don't rebuild them as another widget there. Tree components in libraries vary: check that the one you use nests its groups, or sets the levels, and handles every key, before relying on it.
Fixing this with an AI coding assistant? Get this guide as Markdown, or get every guide in your coding agent.
Verify the fix
5 checks, no mouse.
Test with: Keyboard, Screen reader, Forced colors, Zoom
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the sidebar, its component or its library changes.
Limits
This is a starting pattern, not a guarantee for every application. The scripts are minimal sketches: the tree has no type-ahead, no multiple selection and no loading on demand. A dropdown menu across the top of a page is a different pattern, covered in dropdown menus that open only on hover, and a single section that opens and closes is covered in show and hide buttons that don't say whether they are open. A file tree that also lets people move items by dragging them needs a way without dragging, as drag-and-drop boards that only a mouse can use explains. Rows with several columns, such as an inbox of threads, are a tree grid, a pattern of its own. Test the complete journey with your target assistive technology.
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: treeview (opens in a new tab)
- ARIA APG: treeview-navigation (opens in a new tab)
- ARIA APG: disclosure-navigation (opens in a new tab)
- ARIA APG: keyboard-interface (opens in a new tab)
- WCAG Understanding: info-and-relationships (opens in a new tab)
- WCAG Understanding: keyboard (opens in a new tab)
- WCAG Understanding: name-role-value (opens in a new tab)
- w3.org: wai-aria-1.2 (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.