← Back to Frontend and design systems
See how it helps
2. Composite Widgets: Roving Tabindex vs.
Keyboard and focus behavior
Designs predictable keyboard navigation and visible focus across widgets, overlays, deletion and route changes.
keyboard-focus-engineering
Team members using this skill
When is it useful?
Use when mouse-free navigation gets trapped or loses its current position.
What does it help you do?
- Keeps a logical tab order and avoids positive tabindex workarounds.
- Chooses the appropriate focus model for composite widgets.
- Restores meaningful focus after closing, deleting or navigating.
Try asking this
Fix a table-row action dialog so closing returns focus correctly, including when its row was deleted. Delete the originating row while its dialog is open. On closing, place focus on a meaningful remaining table control instead of the removed button.
Detailed documentation
Read the skill’s original instructions, examples and checks in their authored language.
Show less
Keyboard & Focus Engineering
Every interactive user journey must work end-to-end with a keyboard alone, with a clearly visible focus indicator, predictable tab order and deliberate focus restoration when overlays open, close or remove elements (WCAG 2.1.1, 2.1.2, 2.4.1, 2.4.3, 2.4.7, 2.4.11).
1. Tab Order & Tabindex Discipline
- DOM order drives focus order: arrange elements in the DOM so
TabandShift+Tabfollow the visual reading and task sequence (WCAG 2.4.3). Avoid CSS reordering (flex-direction: row-reverse, arbitraryorderor absolute positioning) that disconnects visual order from DOM tab order. - Tabindex rules:
- Prefer native focusable elements (
<button>,<a href>,<input>,<select>,<textarea>,<summary>). - Use
tabindex="0"only to add a custom widget's active entry point to the tab sequence or make a scrollable region keyboard-scrollable. - Use
tabindex="-1"for elements targeted by programmatic.focus()(headings, dialog containers or inactive items inside a roving-tabindex composite widget). - Never use positive
tabindex(tabindex="1",2, etc.) — it hijacks page-wide tab order.
- Prefer native focusable elements (
- Bypass blocks (WCAG 2.4.1): provide a working "Skip to main content" link as the first focusable item on shells with repeated navigation.
2. Composite Widgets: Roving Tabindex vs. aria-activedescendant
Complex widgets (tablists, menus, toolbars, radio groups, tree views, grids) must be a single tab stop:
Tab/Shift+Tabmoves into and out of the composite widget as a whole; Arrow keys,HomeandEndmove between items inside it.- Roving
tabindexpattern: give the active itemtabindex="0"and all sibling itemstabindex="-1". On arrow key press, movetabindex="0"to the target item, set the previous item to-1, and call.focus(). aria-activedescendantpattern (for comboboxes where focus stays on the text<input>): keep DOM focus on the<input>, updatearia-activedescendant="option-id"onArrowDown/ArrowUp, and scroll the active option into view (scrollIntoView({ block: 'nearest' })).
3. Focus Lifecycle in Overlays, Deletions & Route Changes
Plan where focus goes before, during and after every structural UI transition:
- Opening a modal dialog or drawer:
- Save
document.activeElementbefore opening. - Move initial focus inside the dialog (first primary field, primary action, or dialog heading/container with
tabindex="-1"). - Contain
Tab/Shift+Tabinside the modal (<dialog>.showModal(),inerton background siblings, or focus trap) and close onEscape.
- Save
- Closing a dialog or popover: restore focus to the opener, or to the nearest logical parent/trigger if the opener was unmounted.
- Deleting an item from a list or table: never let focus drop to
<body>. Move focus to the next sibling item, the previous item if the last one was deleted, or the list heading / "Add" button when empty. - Hidden and inert content: ensure elements inside closed drawers, inactive tabs or collapsed panels are unfocusable (
hidden,inertordisplay: none). Never place a focusable element inside anaria-hidden="true"container.
4. Visible Focus Indicators (WCAG 2.4.7 & 2.4.11)
- Use
:focus-visibleto render a high-contrast focus ring (at least2pxsolid outline withoutline-offsetand>= 3:1contrast). - Never use
outline: nonewithout a visible:focus-visiblereplacement that survivesforced-colors: activemode. - Prevent sticky headers or footers from obscuring focused elements (
scroll-margin-top/scroll-margin-bottom).
Included files 1
SKILL.md
