Skip to content

ARIA Focus Behavior Guidelines for naked_ui Components #46

@tilucasoli

Description

@tilucasoli

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

  • Can navigate to every interactive element using only Tab/Shift+Tab
  • Composite widgets have correct arrow key navigation
  • Dialogs trap focus and return it correctly on close
  • Menus/Selects return focus to trigger on close
  • Escape key closes overlays appropriately
  • Disabled elements don't receive focus (unless in composite for discoverability)
  • No focusable elements inside aria-hidden containers
  • Screen reader announces focus changes appropriately

📚 References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions