Widget roles
A widget role tells assistive technology what kind of control something is: a tab, a switch, a slider. Choosing the role is a promise that the keyboard and states will behave like that control. HTML already keeps most of these promises for you. You reach for these roles when you redraw a control yourself.
In one minute
- Use the native element first.
<button>,<input type="checkbox">,<input type="range">,<select>already have the role, the states, and the keys. - A role is only a label.
role="slider"adds no arrow keys and no value. You must build those. - Standalone widgets are one control. Composite widgets (tabs, menus, listboxes, trees, grids) hold many items and share one Tab stop.
- Inside a composite, Tab enters and leaves, and arrow keys move between the items.
A role is like the sign on a door. A sign that says "Exit" makes people expect a way out. If the door is painted on the wall,
the sign is a lie, and people get hurt. role="tab" says "this behaves like a tab". The arrow keys, the selected
state, and the link to its panel are the door behind the sign. You have to build the door.
1. Tabs: one Tab stop, arrows inside
Tabs are the pattern people copy badly. The rules: only the selected tab is in the Tab order. The arrow keys move
between tabs and select them. aria-controls points at the panel, and aria-labelledby on the panel
names it after its tab. Try it, then switch to the broken version and listen to the difference.
Step 1. Click into the tabs, or press Tab once, then use ← → Home End.
Why only one Tab stop? A keyboard user would otherwise press Tab through every tab before reaching the content. With ten tabs that is ten key presses on every visit. With one stop, they skip the whole set in a single press. Screen readers add "tab 2 of 3" and "selected", so people know where they are without seeing the row.
Edge case: this version selects a tab as soon as you arrow onto it ("automatic activation"). If showing a panel is slow, let the arrows move focus only and select on Enter or Space instead.
2. Switch: the native box or a rebuilt one
A switch is an on/off control that takes effect right away. It uses aria-checked, the same state a
checkbox uses. There is no <switch> element, so you either style a checkbox or build one. Compare three versions.
Step 2. Pick a version. Press Tab, then Space to flip it.
| Feature | Result |
|---|
The rebuilt switch needs four things: the role, aria-checked that really changes, tabindex="0", and a key handler for
Space (Enter is optional for a switch). A checkbox gives all of that free, and it still works if your script fails to load.
3. Toolbars and the roving tabindex
"Roving tabindex" is the trick behind tabs, toolbars, menus and radio groups. Exactly one item has tabindex="0". All the
others have tabindex="-1", so they can take focus from a script but Tab skips them. When an arrow key moves, you swap
which item has the 0.
Step 3. Press Tab repeatedly from the "Before" field. Count the presses to reach "After".
Remember where the user left off: when they Shift+Tab back into the toolbar, focus should land on the last item they used, not jump to the first.
That is a free benefit of keeping the 0 on the last-focused item. A toolbar is for a short group of related controls. Do not put a whole form inside one.
4. A custom slider needs a value a person can understand
A native <input type="range"> does all of this for you. If you must draw your own, a slider needs a name,
aria-valuemin, aria-valuemax, aria-valuenow, and keys: arrows step, Home and End jump to the ends.
When the number alone is meaningless, add aria-valuetext.
Step 4. Click the slider, or Tab to it, then use ← → Home End.
A screen reader says "2" for a speed of Express. The words only come through if you provide them. Also make sure a pointer user is not the only one who can drag: every drag needs a keyboard path (WCAG 2.5.7 Dragging Movements, and 2.1.1 Keyboard).
5. Reference: standalone widget roles
Each of these is one control. Look at what each one promises, and prefer the HTML element it names.
| Role | What it promises, and the HTML that already is one |
|---|---|
| button | Activates an action on click, Enter, or Space. Use <button type="button">. Add aria-pressed only for a toggle. |
| link | Navigates. Use <a href>. A button that goes to a URL should be a link. A link that only runs script should be a button. |
| checkbox | Checked true, false, or mixed. Use <input type="checkbox">. |
| radio | One of a group, checked true or false. The group is radiogroup or a fieldset. Use radio inputs that share a name. |
| switch | An on/off checkbox with a different metaphor. Still aria-checked. There is no <switch> element. A checkbox is fine unless the design is literally a switch and you implement it fully. |
| option | A choice inside a listbox. Not focusable by itself when the listbox uses active descendant. Native <option> inside <select>. |
| tab | A label that selects a tabpanel. See the demo. Needs aria-selected and usually aria-controls. |
| tabpanel | The panel for one tab. Name it with aria-labelledby pointing at the tab. |
| menuitem | An action in a menu. Not a navigation link. Application menus are rare on the web. A disclosure of links should stay links. |
| menuitemcheckbox | A menu item that is checked. aria-checked. |
| menuitemradio | A menu item that is one of a radio set. aria-checked. |
| treeitem | A node in a tree. aria-expanded if it has children. aria-level or nesting for depth. |
| gridcell | A cell in an interactive grid, not a layout table. May be focused and edited. |
| progressbar | A completion value. Set valuemin, valuemax, valuenow, or omit valuenow while the work is indeterminate. Native <progress>. |
| scrollbar | A control for scrolling a region. Usually leave this to the browser. If you paint your own, it is a range with orientation and values. |
| searchbox | A textbox intended for search. An <input type="search"> is one. |
| separator | A divider. When it is not focusable it is structure. When it is focusable it is a widget and needs a value, like a split handle. |
| slider | A range the user sets. <input type="range">. Custom sliders need arrow keys, Home, End, valuetext, and a name. |
| spinbutton | A numeric value with up and down. <input type="number"> is the native one. |
| textbox | Text the user edits. <input type="text"> or <textarea>. Set aria-multiline only on a custom one. |
On a website, a "menu" button that opens links is a disclosure (lesson 09), not an ARIA menu. The menu role means application-style action menus with arrow-key behaviour. Using it on a nav list breaks how screen reader users browse links.
6. Reference: composite widget roles
These contain the roles above. One node in the composite is tabbable (the roving tabindex from lab 3). Arrow keys move the active item. If you use
aria-activedescendant, DOM focus stays on the container and the attribute points at the active child's id (lesson 21).
| combobox | An input that controls a popup listbox, grid, or tree. Set aria-expanded and aria-controls. aria-autocomplete says whether suggestions fill the field, the list, or both. The input is editable. The popup is not a second tab stop. |
|---|---|
| grid | An interactive table: cells are focusable, not just data. For static data use a <table> without this role. treegrid is a grid whose rows expand like a tree. |
| listbox | A list of options. Single or multi via aria-multiselectable. A native <select size> is a listbox. |
| menu | A set of menuitems for actions. Arrow keys, not tab, move inside. Escape closes. Do not use menu for site navigation. |
| menubar | A horizontal bar of menus, the desktop pattern. Rarely correct for a website header. |
| radiogroup | Contains radios. A <fieldset> with a legend is the HTML version and names the group. |
| tablist | Contains tabs. Arrow keys move. The demo above is one. Name the tablist if there are two on the page. |
| tree | Contains treeitems. Expand and collapse with arrows. Selection is separate from expansion. |
| toolbar | A group of controls that arrow-key as one tab stop, such as an editor format bar. Do not put a whole form in a toolbar. |
Common mistakes
| You see | Why it is a problem | Fix |
|---|---|---|
role="button" on a div, no tabindex | Announced as a button but Tab skips it. | Use <button>. |
role="slider" with no aria-valuenow or keys | A promise with nothing behind it. | Value, min, max, arrow keys, Home, End. |
Every tab has tabindex="0" | Keyboard users Tab through all of them. | Roving tabindex. |
role="menu" on a site navigation | Changes how links are browsed. | Use a <nav> with links. |
aria-checked on a native checkbox | Can disagree with the real checked state. | Let the browser manage it. |
aria-selected never updated | Everyone hears "selected" on the wrong tab. | Change the attribute with the state. |
- In lab 1, Tab to "Pickup", use → twice, then Shift+Tab away and Tab back. Where does focus land?
- In lab 3, switch to roving tabindex and count Tab presses again.
- Open the browser's accessibility panel and find the
tablistand its threetabchildren.
Remember
- Native element first. A role is a promise you must keep.
- Composite widgets: one Tab stop, arrow keys inside, roving tabindex.
- Every state attribute (
aria-selected,aria-checked,aria-valuenow) must change when the UI changes. - Give numeric widgets
aria-valuetextwhen the number is not meaningful alone.