Tabs, accordions, menus and carousels are where accessibility problems pile up. Here is a short, practical approach.
Step 1: can native HTML do it?
| Widget | Native option |
|---|---|
| Show/hide section | <details> / <summary> |
| Popup | <dialog> |
| Choose one of many | <select>, radio buttons |
| Slider | <input type="range"> |
| Progress | <progress>, <meter> |
| Autocomplete suggestions | <datalist> |
If one fits, use it, and style it with CSS.
Step 2: the checklist for anything you build
- Use real elements (
buttonfor actions,afor navigation). - Keyboard works: Tab reaches it, Enter and Space activate, Escape closes, arrows move inside groups.
- Visible focus at all times.
- State is announced:
aria-expanded,aria-selected,aria-pressed,aria-current, kept in sync with what you see. - A name for everything: visible text,
aria-label, oraria-labelledby. - Do not hide content that must be available by only
opacity: 0. Usehiddenordisplay: nonefor what should be unreachable. - Respect user preferences:
prefers-reduced-motionfor animation. - Manage focus when content opens, closes or changes (Focus Management).
An accessible accordion in a few lines
Buttons control panels, and the state is exposed with ARIA.
<style>
.acc button { display: block; width: 100%; text-align: left; padding: 8px; background: #f4f9fc; border: 1px solid #1a5276; }
.acc button:focus-visible { outline: 3px solid #d94a00; }
.acc [role="region"] { padding: 8px; border: 1px solid #ddd; border-top: 0; }
</style>
<div class="acc" id="acc">
<h3><button aria-expanded="false" aria-controls="p1" id="b1">What is JavaScript?</button></h3>
<div role="region" id="p1" aria-labelledby="b1" hidden>The language of the web.</div>
<h3><button aria-expanded="false" aria-controls="p2" id="b2">Is it hard?</button></h3>
<div role="region" id="p2" aria-labelledby="b2" hidden>Not with practice.</div>
</div>
<script>
document.querySelector("#acc").addEventListener("click", event => {
const button = event.target.closest("button[aria-controls]");
if (!button) return;
const open = button.getAttribute("aria-expanded") === "true";
button.setAttribute("aria-expanded", String(!open));
document.getElementById(button.getAttribute("aria-controls")).hidden = open;
});
</script>
What makes it accessible:
- The headers are real
<button>elements inside headings, so they are focusable and activated with Enter and Space with no extra code. aria-expandedannounces the state,aria-controlslinks to the panel.- Panels use
hidden, so closed content is not reachable.
Tabs: an arrow-key pattern
Tabs use role="tablist", role="tab" with aria-selected, and role="tabpanel". Only the selected tab is in the Tab order (tabindex="0", others -1), and arrow keys move between tabs, as in the toolbar example in Keyboard Interaction. The Tabs project puts it together.
Menus: be careful with the word “menu”
A site navigation menu is a list of links (<nav> with <ul>) plus a toggle button using aria-expanded. It is not an application menu with role="menu", which promises arrow-key behaviour that most site menus do not implement.
Carousels and auto-moving content
If something moves automatically for more than five seconds, give a pause button, and do not start moving at all when the user prefers reduced motion.
Test, test, test
- Keyboard only.
- A screen reader for 5 minutes.
- Zoomed to 200%.
- With a reduced motion setting.
The W3C ARIA Authoring Practices Guide shows tested patterns for most widgets.