What is a disclosure, and why does its state matter?
A disclosure is a control that shows or hides a piece of content: a mobile menu, a question on a help page, a list of allergens, a “More filters” panel. Visually the change is obvious. The links slide in, an arrow turns, the page grows. For someone using a screen reader, the button sounds the same before and after, and the content may have appeared somewhere they are not reading.
That is why a disclosure button needs a state. With aria-expanded="false" a screen reader announces “Menu, button, collapsed”, which tells the user there is something hidden here. After a press, “expanded” confirms that it opened. The state is the whole conversation between the control and the user, and without it they are guessing.
On the starting page, the Screen reader view says button, Menu and nothing more, whether the links are showing or not. Press it in the Page view, read the view again, and compare.
How do you build a menu button with `aria-expanded`?
Start with a real <button type="button">. It is focusable, works with Enter and Space, and has the button role, so you only need to add the state. Give it aria-expanded="false" in the HTML, because the menu starts closed. Give it aria-controls with the id of the element it opens.
In the click handler, read the current state, flip it, and show or hide the content to match. Keeping the attribute and the visibility in one place means they can never disagree. Hide the content with the hidden attribute or display: none, so the hidden links are also out of the Tab order and out of the accessibility tree.
Leave the button’s name alone when it opens. Changing “Menu” to “Close menu” as well as setting aria-expanded makes screen readers announce “Close menu, expanded”, which is muddled. Pick one: a stable name with a state is the clearer choice.
<button type="button" id="menu-btn" aria-expanded="false" aria-controls="site-menu">Menu</button>
<ul id="site-menu" hidden> ... </ul>
<script>
btn.addEventListener('click', () => {
const open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
menu.hidden = open;
});
</script>

When can `<details>` and `<summary>` do the job instead?
The <details> element is a disclosure built into HTML. Its <summary> is a button that toggles the rest of the content, with the expanded or collapsed state exposed to screen readers automatically, and keyboard support with no script at all. For FAQs, extra product information and “show more” sections it is the simplest correct choice.
It has limits. The summary’s text is its name, so keep it short and meaningful. Headings inside a summary are announced oddly by some screen readers; put the heading just before the <details> instead, or leave it out. Styling the marker takes a little CSS. And a <details> element cannot easily control content elsewhere on the page, which is why a site menu in a header usually uses a button.
Since 2024, several <details> elements can share a name attribute, so opening one closes the others, like an accordion. That is fine for help pages, though many people prefer to open several questions at once.
<details>
<summary>Can you make it gluten free?</summary>
<p>Yes. We bake gluten-free sponges on Mondays, in a separate oven.</p>
</details>Why is `role="menu"` wrong for site navigation?
ARIA has a menu role, and it sounds like the right one for a navigation menu. It is meant for application menus, like the File and Edit menus in a desktop program. When a screen reader meets role="menu", it switches to application behaviour: it expects the arrow keys to move between items, Tab to leave the whole menu, and often stops reading the items as links.
If your code does not provide those arrow key handlers, the user is stuck with a widget that promises one thing and does another. Even when it does, site visitors expect links to behave like links: Tab moves through them, they show in the links list, and they can be opened in a new tab.
Site navigation is a list of links inside a <nav>. Name the <nav> with aria-label if there is more than one. Use aria-current="page" on the link to the current page. That is all it needs. The rule from the ARIA lesson holds: no ARIA is better than bad ARIA.
How should a mobile menu behave with a keyboard?
A keyboard user should be able to Tab to the Menu button, press Enter or Space to open it, and Tab straight into the links. That means the list should come right after the button in the source, or close to it. If the open menu is placed at the end of the page with CSS positioning, Tab goes everywhere else first.
Escape is a helpful extra for menus that cover the page: close the menu and put focus back on the button. A menu that only pushes content down, like the demo, does not need it. When the menu is closed, its links must be hidden properly, so they are not Tab stops that lead nowhere visible.
Check it in the Keyboard view. Press Tab to reach Menu, then Enter, then Tab again. The next stop should be Repairs, and the Tab list should grow by four links.
- Enter or Space opens and closes the menu
- The first Tab after opening reaches the first link
- Closed links are fully hidden instead of moved off screen
- The button says collapsed or expanded
What about dropdowns in a desktop navigation bar?
Menus that open on hover are a problem for keyboards, touch screens and people with tremors. A sub-menu that only opens on :hover cannot be reached by Tab at all. One that closes the moment the pointer drifts a few pixels away fails 1.4.13 Content on Hover or Focus, which asks for hover content to stay open while the pointer is over it, and to be dismissable.
The sturdy pattern is the same disclosure button you built here. Put a button next to each top level link, or make the top level item itself a button, and open the sub-list on click or Enter. Hover can be an extra on top. Keep the sub-lists as plain links.
Test it with the keyboard first, then with your phone, then with a mouse moved slowly from the top item down into the list.
What do AI tools get wrong with menus?
Generated navigation often arrives as <div class="hamburger" onclick="toggle()"> with three spans inside for the lines. That gives a control with no role, no name and no keyboard support. Others use a real button and forget the state, or set aria-expanded="true" in the HTML and never update it, so it lies half the time.
AI tools also reach for role="menu", role="menubar" and role="menuitem" on ordinary links, because the words match. They write aria-haspopup="true" on buttons that open a simple list, which tells screen readers a menu widget is coming. And they hide the closed menu with opacity: 0 or by moving it off screen, which leaves invisible links in the Tab order.
When you review, press the toggle and then read the Screen reader view and the Keyboard view together. The state and the Tab list should change at the same moment.
Field notes
- A control that shows and hides content must say whether it is open.
- Use
aria-expandedon a real<button>and update it in your script. <details>and<summary>give you a built-in show and hide control.- Keep site navigation as plain links instead of
role="menu".
Easy to get wrong
A div cannot be reached with Tab and has no role or name. A real button works with the keyboard and can carry the expanded state.
Menu roles are for application menus with arrow key handling. On site navigation they change how screen readers behave and remove the links from what users expect.
A state that does not follow the content is worse than none. Update it in the same place you show and hide the content.
Questions people ask
Is `aria-controls` required?
It is recommended. Support varies, and some screen readers do little with it, but it costs nothing and documents the connection. The state from aria-expanded is the part users depend on.
Should the menu icon be three lines or the word Menu?
The word Menu is understood by more people. If you use the icon, give the button an accessible name, and consider showing the word next to the icon. Voice control users need a name they can say.
Can I use a checkbox hack to open the menu without JavaScript?
It is announced as a checkbox, which is confusing for a menu. A <details> element is a better no-script option, or a button with a few lines of script.
Do I need `aria-hidden` on the closed menu?
No. The hidden attribute and display: none already remove it from the accessibility tree. Adding aria-hidden as well only creates a second thing to keep in sync.