On this page
Site navigation often borrows the word menu from restaurant lists and desktop applications, but those interfaces do not share one interaction model. A header containing links to product pages usually needs ordinary link navigation with expandable sections. Giving it application-menu roles can make familiar links harder to discover and creates keyboard responsibilities the implementation may not fulfill.
Distinguish navigation from application commands
Start by identifying the content. Links to pricing, documentation, and account pages are navigation destinations. Commands such as changing a drawing tool may justify an application-style menu. Choose semantics from the task, not from the fact that a panel appears beneath a button.
Use the Flexbox Playground to prototype header wrapping and spacing while preserving a logical source sequence. Keep each disclosure button adjacent to the links it controls. This makes source order easier to understand when styles are unavailable or the header collapses on a small screen.
The WAI disclosure navigation example explains why typical site navigation does not need menu and menuitem roles. Its examples also emphasize testing assistive technology support rather than copying demonstration code without verification.
Keep links and toggle buttons distinct
If a heading both navigates and expands, split those actions into separate controls. A Products link can lead to the overview while an adjacent button expands product links. Do not make the first click expand and the second click navigate; that hidden rule is difficult to discover on touchscreens and through voice control.
<nav aria-label="Main">
<ul>
<li>
<button type="button" aria-expanded="false"
aria-controls="product-links" id="product-toggle">
Products
</button>
<ul id="product-links" hidden>
<li><a href="/products/editor">Editor</a></li>
<li><a href="/products/reports">Reports</a></li>
</ul>
</li>
</ul>
</nav>The button supplies native Enter and Space activation. The links remain ordinary Tab stops when visible. There is no need to add a roving tabindex system, arrow-key-only navigation, or a menu role simply because the design resembles a desktop dropdown.
If you include an arrow icon, keep it decorative unless it conveys information missing from the button's state. A screen reader should announce the control's name and expanded state, not a filename or a redundant description of a down-pointing triangle.
Synchronize visibility and expanded state
The disclosure has two representations of one fact: whether the panel is available. Update both together. A collapsed panel must not leave invisible links in the Tab sequence, while an expanded panel must expose its content to assistive technology.
const toggle = document.querySelector('#product-toggle');
const links = document.querySelector('#product-links');
function setOpen(open) {
toggle.setAttribute('aria-expanded', String(open));
links.hidden = !open;
}
toggle.addEventListener('click', () => {
setOpen(toggle.getAttribute('aria-expanded') !== 'true');
});
links.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
setOpen(false);
toggle.focus();
}
});Avoid CSS that accidentally overrides the hidden attribute with a permanent display rule. If you animate closing, ensure content stops being interactive at the right time rather than leaving a fading but focusable panel in the page. Simpler immediate visibility changes are often more robust than elaborate transitions.
The MDN aria-expanded reference explains how the state belongs on the controlling element. It does not belong only on the panel, where users activating the button would miss it.
Make dismissal predictable across input methods
For a disclosure, opening normally leaves focus on the trigger. The next Tab reaches the first revealed link. This preserves ordinary page navigation and avoids unexpectedly sending the user into a panel they may only be exploring. Closing from inside with Escape should return focus to the trigger.
Clicking outside can close the panel as an extra convenience. It must not be the only dismissal route. Be cautious about closing on blur alone: focus briefly moves during many legitimate interactions, and a badly timed handler can remove the link before it activates.
Hover support should supplement clicking, not replace it. If content appears on hover or focus, consider the WCAG requirements for dismissibility, hoverability, and persistence. Users must be able to move the pointer into the revealed panel without crossing a tiny gap that immediately hides it.
Multiple open sections can be valid, as can a one-open-at-a-time rule. Whichever you choose, do not close a section that currently contains keyboard focus without moving that focus somewhere sensible. Removing the active element creates an avoidable interruption and makes the next Tab destination unpredictable.
Verify responsive and assistive technology behavior
Test the header before and after the mobile breakpoint. A collapsed mobile navigation button needs its own expanded state and a clear relationship to its panel. Switching breakpoints should not leave a focused desktop link hidden inside an unavailable mobile region. Resize while focus is inside an open section to expose these state bugs.
Use the contrast checker for text and focus styling on each header surface. Check that selected-page styling is not communicated by color alone. Where appropriate, aria-current="page" identifies the current destination independently of the disclosure's expanded state.
Run a task that opens a section, follows a link, returns, and opens another section. Repeat using only the keyboard and then a screen reader. Listen for clear button names, collapsed or expanded announcements, and the expected number of ordinary links. A navigation landmark label should distinguish this header from footer or account navigation.
The WCAG content on hover or focus guidance helps assess floating panels. Store the chosen behavior with other accessibility patterns, including Escape handling, breakpoint transitions, and whether multiple sections may stay open. Those details are where inconsistent implementations usually emerge.
Make failure and enhancement states usable
The minimal example assumes its script initializes successfully. If the panel begins hidden and JavaScript fails, those destinations disappear altogether. Plan a progressive enhancement strategy: render essential links visibly by default, then activate collapsing only after the controller is ready, or provide an always-available overview page containing the same destinations. Do not leave a working-looking toggle that does nothing.
When a framework hydrates the header, keep initial visibility and expanded state consistent between the server response and the client. A mismatch can briefly expose hidden links or move focus during initialization. Test a slow connection and a failed script request, not only the fully loaded application. Critical navigation should remain discoverable even when nonessential enhancement fails.
Use a short interaction checklist for every header variant. Open Products with Enter, Tab through its links, close with Escape, then reopen with Space. Move the pointer between the trigger and panel, zoom until the header wraps, and change orientation while a link has focus. Confirm that each action preserves a visible focus target and an accurate expanded announcement.
Finally, test translated labels and a larger default font. A trigger that fits in English may wrap onto two lines, altering hover boundaries and panel placement. Prefer layout that tolerates that change over fixed coordinates calculated for one screenshot. Good disclosure behavior depends on the rendered content, not just the original component dimensions.
Key takeaways
- Use disclosure buttons for ordinary expandable site navigation.
- Keep navigation destinations separate from expansion actions.
- Synchronize aria-expanded with real content visibility.
- Preserve ordinary Tab navigation and predictable Escape behavior.
- Test hover, touch, keyboard, and responsive state transitions separately.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor