Lesson 18 · Part 4 · Widget roles

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

In plain words

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.

Collect from the dock after 16:00.

          

Move to a tab to see the verdict.

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.


          
What each version gives you
FeatureResult

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".

Tab through to count the stops.

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.


          
Move the slider.

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.

RoleWhat it promises, and the HTML that already is one
buttonActivates an action on click, Enter, or Space. Use <button type="button">. Add aria-pressed only for a toggle.
linkNavigates. Use <a href>. A button that goes to a URL should be a link. A link that only runs script should be a button.
checkboxChecked true, false, or mixed. Use <input type="checkbox">.
radioOne of a group, checked true or false. The group is radiogroup or a fieldset. Use radio inputs that share a name.
switchAn 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.
optionA choice inside a listbox. Not focusable by itself when the listbox uses active descendant. Native <option> inside <select>.
tabA label that selects a tabpanel. See the demo. Needs aria-selected and usually aria-controls.
tabpanelThe panel for one tab. Name it with aria-labelledby pointing at the tab.
menuitemAn action in a menu. Not a navigation link. Application menus are rare on the web. A disclosure of links should stay links.
menuitemcheckboxA menu item that is checked. aria-checked.
menuitemradioA menu item that is one of a radio set. aria-checked.
treeitemA node in a tree. aria-expanded if it has children. aria-level or nesting for depth.
gridcellA cell in an interactive grid, not a layout table. May be focused and edited.
progressbarA completion value. Set valuemin, valuemax, valuenow, or omit valuenow while the work is indeterminate. Native <progress>.
scrollbarA control for scrolling a region. Usually leave this to the browser. If you paint your own, it is a range with orientation and values.
searchboxA textbox intended for search. An <input type="search"> is one.
separatorA 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.
sliderA range the user sets. <input type="range">. Custom sliders need arrow keys, Home, End, valuetext, and a name.
spinbuttonA numeric value with up and down. <input type="number"> is the native one.
textboxText the user edits. <input type="text"> or <textarea>. Set aria-multiline only on a custom one.
Menus are rarely the right answer

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).

comboboxAn 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.
gridAn 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.
listboxA list of options. Single or multi via aria-multiselectable. A native <select size> is a listbox.
menuA set of menuitems for actions. Arrow keys, not tab, move inside. Escape closes. Do not use menu for site navigation.
menubarA horizontal bar of menus, the desktop pattern. Rarely correct for a website header.
radiogroupContains radios. A <fieldset> with a legend is the HTML version and names the group.
tablistContains tabs. Arrow keys move. The demo above is one. Name the tablist if there are two on the page.
treeContains treeitems. Expand and collapse with arrows. Selection is separate from expansion.
toolbarA 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 seeWhy it is a problemFix
role="button" on a div, no tabindexAnnounced as a button but Tab skips it.Use <button>.
role="slider" with no aria-valuenow or keysA 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 navigationChanges how links are browsed.Use a <nav> with links.
aria-checked on a native checkboxCan disagree with the real checked state.Let the browser manage it.
aria-selected never updatedEveryone hears "selected" on the wrong tab.Change the attribute with the state.
Try it yourself
  1. In lab 1, Tab to "Pickup", use → twice, then Shift+Tab away and Tab back. Where does focus land?
  2. In lab 3, switch to roving tabindex and count Tab presses again.
  3. Open the browser's accessibility panel and find the tablist and its three tab children.

Remember

Edit this example in the playground