This issue describes how keyboard focus should behave according to WAI-ARIA Authoring Practices Guide (APG) for each component in the naked_ui library. Proper focus management is critical for accessibility—ensuring keyboard and assistive technology users can navigate predictably and efficiently.
🎯 Key Principles (Apply to All Components)
| Principle |
Description |
| Logical Tab Order |
Tab sequence must follow reading order (top-to-bottom, left-to-right in LTR languages). |
| Single Tab Stop for Composites |
Composite widgets (tabs, menus, radio groups) should have only ONE element in the global Tab sequence; internal navigation uses arrow keys. |
| Focus Persistence |
When content is removed or overlays close, focus must return to a logical element—never leave focus "nowhere" (body). |
| Never Hide Focusable Elements |
Elements with aria-hidden="true" must NOT be focusable. |
🔧 Component-Specific Focus Behavior
1. Button (NakedButton)
Role: button
| Action |
Expected Behavior |
| Tab |
Button receives focus in natural tab order |
| Shift+Tab |
Button loses focus to previous focusable element |
| Enter / Space |
Activates the button |
Notes:
- Disabled buttons (
aria-disabled="true") should generally NOT receive focus
- Toggle buttons should announce their pressed/unpressed state
2. Checkbox (NakedCheckbox)
Role: checkbox
| Action |
Expected Behavior |
| Tab |
Checkbox receives focus in natural tab order |
| Space |
Toggles checked state |
States:
aria-checked="true" — checked
aria-checked="false" — unchecked
aria-checked="mixed" — indeterminate (for tri-state checkboxes)
Notes:
- Each checkbox is an independent tab stop
- Disabled checkboxes should NOT receive focus
3. Radio Group (NakedRadio)
Role: radiogroup (container) + radio (items)
| Action |
Expected Behavior |
| Tab |
Focus enters the radio group on the checked radio; if none checked, focus goes to first radio |
| Tab (exit) |
Focus leaves the entire radio group to next focusable element |
| Arrow Down / Arrow Right |
Moves focus to next radio (and selects it in most implementations) |
| Arrow Up / Arrow Left |
Moves focus to previous radio (and selects it) |
| Space |
Selects the focused radio (if not already selected) |
| Home |
(Optional) Moves focus to first radio |
| End |
(Optional) Moves focus to last radio |
Focus Management:
- ✅ Uses roving
tabindex — only one radio has tabindex="0"
- ✅ The entire group is ONE tab stop
- ✅ Arrow keys wrap (optional, but recommended)
4. Toggle/Switch (NakedToggle)
Role: switch
| Action |
Expected Behavior |
| Tab |
Switch receives focus in natural tab order |
| Space |
Toggles the switch on/off |
| Enter |
(Optional) May also toggle |
States:
aria-checked="true" — on
aria-checked="false" — off
5. Slider (NakedSlider)
Role: slider
| Action |
Expected Behavior |
| Tab |
Slider thumb receives focus |
| Arrow Right / Arrow Up |
Increases value by one step |
| Arrow Left / Arrow Down |
Decreases value by one step |
| Page Up |
Increases value by larger step (e.g., 10%) |
| Page Down |
Decreases value by larger step |
| Home |
Sets value to minimum |
| End |
Sets value to maximum |
Required Attributes:
aria-valuenow — current value
aria-valuemin — minimum value
aria-valuemax — maximum value
aria-valuetext — (optional) human-readable value
Notes:
- For range sliders with two thumbs, each thumb is a separate tab stop
6. Tabs (NakedTabs)
Role: tablist (container), tab (tabs), tabpanel (panels)
| Action |
Expected Behavior |
| Tab (into tablist) |
Focus lands on the selected tab; if none selected, first tab |
| Tab (from tablist) |
Focus moves to the tab panel content (or next focusable outside) |
| Arrow Left / Arrow Up |
Moves focus to previous tab |
| Arrow Right / Arrow Down |
Moves focus to next tab |
| Home |
Moves focus to first tab |
| End |
Moves focus to last tab |
| Space / Enter |
Activates the focused tab (in manual activation mode) |
Activation Modes:
| Mode |
Behavior |
| Automatic |
Moving focus with arrows immediately activates (shows) the tab panel |
| Manual |
Arrow keys only move focus; user must press Space/Enter to activate |
Focus Management:
- ✅ Uses roving
tabindex within tablist
- ✅ Tablist is ONE tab stop
- ✅ Tab panels should be focusable only if they contain no focusable content, OR focus moves directly to first focusable inside panel
Arrow Key Wrapping:
- Arrows should wrap from last tab to first (and vice versa)
7. Accordion (NakedAccordion)
Role: Uses button for headers (or heading + button), region for panels (optional)
| Action |
Expected Behavior |
| Tab |
Moves focus between accordion headers (each header is a tab stop) |
| Enter / Space |
Toggles the accordion panel open/closed |
| Arrow Down |
(Optional) Moves focus to next accordion header |
| Arrow Up |
(Optional) Moves focus to previous accordion header |
| Home |
(Optional) Moves focus to first accordion header |
| End |
(Optional) Moves focus to last accordion header |
Focus Behavior:
- Each accordion header/trigger is in the tab order
- When a panel opens, focus stays on the trigger (does NOT auto-move into panel)
- Content inside expanded panels is reachable via Tab
Notes:
- If using optional arrow key navigation, treat headers as a composite widget with roving tabindex
- Without arrow keys, each header is simply a normal tab stop
8. Menu (NakedMenu)
Role: menu (container), menuitem / menuitemcheckbox / menuitemradio (items)
| Action |
Expected Behavior |
| Click/Enter on Trigger |
Opens menu, focus moves to first menu item |
| Arrow Down |
Moves focus to next menu item |
| Arrow Up |
Moves focus to previous menu item |
| Home |
Moves focus to first menu item |
| End |
Moves focus to last menu item |
| Enter / Space |
Activates focused menu item |
| Escape |
Closes menu, returns focus to trigger |
| Tab |
Closes menu, moves focus to next focusable (or trigger) |
| Type-ahead |
Typing characters moves focus to matching item |
For Submenus:
- Arrow Right — Opens submenu, focus on first item
- Arrow Left / Escape — Closes submenu, focus returns to parent item
Focus Management:
- ✅ Uses roving
tabindex inside menu
- ✅ Menu trigger is in tab order; menu items are NOT
- ✅ Focus must be trapped inside open menu
- ✅ On close, focus MUST return to the trigger element
9. Select / Combobox (NakedSelect)
Role: combobox (input), listbox (dropdown), option (items)
| Action |
Expected Behavior |
| Tab |
Combobox trigger/input receives focus |
| Enter / Space / Arrow Down |
Opens dropdown, focus moves to selected option (or first) |
| Arrow Down (open) |
Moves focus to next option |
| Arrow Up (open) |
Moves focus to previous option |
| Home (open) |
Moves focus to first option |
| End (open) |
Moves focus to last option |
| Enter / Space (open) |
Selects focused option, closes dropdown |
| Escape |
Closes dropdown without selecting, returns focus to trigger |
| Tab (open) |
Selects focused option, closes dropdown, moves to next element |
| Type-ahead |
Filters or jumps to matching option |
Focus Management Options:
| Pattern |
Description |
| DOM Focus |
Focus actually moves to options in the listbox |
aria-activedescendant |
Focus stays on combobox; aria-activedescendant points to active option |
Notes:
- When dropdown closes, focus MUST return to trigger
- Selected option should be announced to screen readers
- For multi-select: Space toggles selection, maintains dropdown open
10. Dialog/Modal (NakedDialog)
Role: dialog or alertdialog
On Open:
| Behavior |
Description |
| Move focus into dialog |
Focus should move to the first focusable element, OR a primary action button, OR the dialog title (tabindex="-1") if content is lengthy |
| Trap focus |
Tab/Shift+Tab must cycle within the dialog only |
| Disable background |
Background content should be aria-hidden="true" and not receive focus |
While Open:
| Action |
Expected Behavior |
| Tab |
Cycles forward through focusable elements in dialog |
| Shift+Tab |
Cycles backward through focusable elements |
| Escape |
Closes dialog (for dismissible dialogs) |
On Close:
| Behavior |
Description |
| Return focus |
Focus MUST return to the element that opened the dialog |
| Fallback |
If trigger no longer exists, focus should go to a logical alternative |
Focus Trap Implementation:
First focusable ← Shift+Tab ← Last focusable
Last focusable → Tab → First focusable
11. Popover (NakedPopover)
Role: Depends on content (may use dialog for interactive popovers)
Non-Modal Popover (e.g., information popover):
| Action |
Expected Behavior |
| Click/Enter on Trigger |
Opens popover |
| Focus behavior |
If popover has focusable content, focus MAY move into it; otherwise stays on trigger |
| Escape |
Closes popover |
| Click outside |
Closes popover |
| Tab |
If focus in popover, tabs through content; then closes and continues |
Interactive Popover (with focusable content):
| Action |
Expected Behavior |
| On open |
Focus should move to first focusable element in popover |
| On close |
Focus returns to trigger |
Notes:
- Unlike modal dialogs, popovers generally don't trap focus
- Escape should always close the popover
- Consider using
aria-haspopup on trigger
12. Tooltip (NakedTooltip)
Role: tooltip
| Action |
Expected Behavior |
| Focus on trigger |
Tooltip appears |
| Blur from trigger |
Tooltip disappears |
| Escape |
Tooltip disappears (while trigger still focused) |
Focus Behavior:
- ⚠️ Tooltips should NOT be focusable themselves
- Focus stays on the trigger element
- Tooltip is purely supplementary information
- Use
aria-describedby to associate tooltip content with trigger
Notes:
- If tooltip content needs to be interactive, use a Popover instead
- Must be dismissible via Escape without moving focus
13. TextField (NakedTextField)
Role: textbox (native <input> or <textarea>)
| Action |
Expected Behavior |
| Tab |
Field receives focus |
| Typing |
Characters entered into field |
| Arrow Left/Right |
Moves cursor within text |
| Home/End |
Moves cursor to start/end |
| Shift+Arrows |
Text selection |
| Ctrl/Cmd+A |
Select all |
Associated Elements:
- Labels should be properly associated via
aria-labelledby or <label for>
- Error messages via
aria-describedby and aria-invalid="true"
- Required fields via
aria-required="true"
📊 Focus Behavior Summary Table
| Component |
Tab Stops |
Internal Navigation |
On Open Focus |
On Close Focus |
| Button |
1 |
N/A |
N/A |
N/A |
| Checkbox |
1 each |
N/A |
N/A |
N/A |
| Radio Group |
1 (group) |
Arrow keys |
N/A |
N/A |
| Toggle/Switch |
1 |
N/A |
N/A |
N/A |
| Slider |
1 per thumb |
Arrow/Home/End/Page |
N/A |
N/A |
| Tabs |
1 (tablist) |
Arrow keys |
N/A |
N/A |
| Accordion |
1 per header (or roving) |
Optional arrows |
Stays on trigger |
N/A |
| Menu |
1 (trigger) |
Arrow keys |
First item |
Return to trigger |
| Select |
1 (trigger) |
Arrow keys |
Selected/first option |
Return to trigger |
| Dialog |
1+ (trapped) |
Tab cycles |
First focusable |
Return to trigger |
| Popover |
1+ (not trapped) |
Tab |
First focusable (if interactive) |
Return to trigger |
| Tooltip |
0 (not focusable) |
N/A |
N/A |
N/A |
| TextField |
1 |
Arrow keys (cursor) |
N/A |
N/A |
🧪 Testing Checklist
📚 References
This issue describes how keyboard focus should behave according to WAI-ARIA Authoring Practices Guide (APG) for each component in the
naked_uilibrary. Proper focus management is critical for accessibility—ensuring keyboard and assistive technology users can navigate predictably and efficiently.🎯 Key Principles (Apply to All Components)
aria-hidden="true"must NOT be focusable.🔧 Component-Specific Focus Behavior
1. Button (
NakedButton)Role:
buttonNotes:
aria-disabled="true") should generally NOT receive focus2. Checkbox (
NakedCheckbox)Role:
checkboxStates:
aria-checked="true"— checkedaria-checked="false"— uncheckedaria-checked="mixed"— indeterminate (for tri-state checkboxes)Notes:
3. Radio Group (
NakedRadio)Role:
radiogroup(container) +radio(items)Focus Management:
tabindex— only one radio hastabindex="0"4. Toggle/Switch (
NakedToggle)Role:
switchStates:
aria-checked="true"— onaria-checked="false"— off5. Slider (
NakedSlider)Role:
sliderRequired Attributes:
aria-valuenow— current valuearia-valuemin— minimum valuearia-valuemax— maximum valuearia-valuetext— (optional) human-readable valueNotes:
6. Tabs (
NakedTabs)Role:
tablist(container),tab(tabs),tabpanel(panels)Activation Modes:
Focus Management:
tabindexwithin tablistArrow Key Wrapping:
7. Accordion (
NakedAccordion)Role: Uses
buttonfor headers (orheading+button),regionfor panels (optional)Focus Behavior:
Notes:
8. Menu (
NakedMenu)Role:
menu(container),menuitem/menuitemcheckbox/menuitemradio(items)For Submenus:
Focus Management:
tabindexinside menu9. Select / Combobox (
NakedSelect)Role:
combobox(input),listbox(dropdown),option(items)Focus Management Options:
aria-activedescendantaria-activedescendantpoints to active optionNotes:
10. Dialog/Modal (
NakedDialog)Role:
dialogoralertdialogOn Open:
tabindex="-1") if content is lengthyaria-hidden="true"and not receive focusWhile Open:
On Close:
Focus Trap Implementation:
11. Popover (
NakedPopover)Role: Depends on content (may use
dialogfor interactive popovers)Non-Modal Popover (e.g., information popover):
Interactive Popover (with focusable content):
Notes:
aria-haspopupon trigger12. Tooltip (
NakedTooltip)Role:
tooltipFocus Behavior:
aria-describedbyto associate tooltip content with triggerNotes:
13. TextField (
NakedTextField)Role:
textbox(native<input>or<textarea>)Associated Elements:
aria-labelledbyor<label for>aria-describedbyandaria-invalid="true"aria-required="true"📊 Focus Behavior Summary Table
🧪 Testing Checklist
aria-hiddencontainers📚 References