Skip to main content

Focus management patterns

Focus management patterns define how keyboard navigation and focus behavior work across components. These patterns ensure accessible, predictable interactions for all users, including those who rely on keyboard navigation.

Component patterns

Modal dialog


  Confirm Action
  
    

Are you sure?

Behavior:

  • Focus traps within dialog
  • Focus moves to first interactive element on open
  • Escape closes dialog
  • Focus returns to trigger element on close

Common mistakes:

  • Not trapping focus, allowing Tab to exit modal
  • Not restoring focus to trigger on close
  • Missing aria-modal="true"

Alert dialog (critical)

For destructive or critical confirmations where Escape should NOT close.


  Delete Item?
  

This action cannot be undone.

Key difference: Focus the LEAST destructive option (Cancel) by default.

Dropdown menu


  {items.map((item, index) => (
    
      {item.label}
    
  ))}

Keyboard navigation:

  • ↑/↓: move between items (wraps at boundaries)
  • Enter / Space: activate item
  • Escape: close menu
  • Tab: exit menu (does NOT navigate items)
  • Home / End: jump to first/last item
  • Type character: jump to item starting with that letter

Tabs

{tabs.map((tab, index) => ( ))}
{tabs.map((tab, index) => ( ))}

Keyboard navigation:

  • ←/→: move between tabs (wraps)
  • Usually automatic activation (arrow changes tab)
  • Tab key: exit tablist to panel content

Toolbar

{tools.map((tool, index) => ( ))}

Key behavior: Toolbar is ONE tab stop. Use arrow keys to move between items.

Combobox / select

{isOpen && (
    {options.map((option) => (
  • {option.label}
  • ))}
)}

Key difference: Arrow keys do NOT wrap: stop at first/last option.

Skip links


  Skip to main content


{/* Later in the page */}
{/* Content */}

Requirements:

  • Must be first focusable element on page
  • Hidden until focused
  • Target needs tabIndex="-1" to receive programmatic focus

Focus rules

Visibility rules

RuleRequirementWCAG
Visible focus indicatorAll interactive elements must show focus2.4.7 (AA)
Use focus-visibleShow focus only on keyboard nav, not clicksBest practice
Focus contrastFocus indicator must have 3:1 contrast against adjacent colours1.4.11 (AA)
Focus appearanceIndicator area and contrast thresholds2.4.13 (AAA)

The exact ring (width, colour, whether it carries an offset) is the design system's to declare, so check get_interaction_states (or get_design_tokens for the token values) rather than copying a fixed value. get_focus_pattern is not a reliable source for it: its shape is focus management behaviour plus focusRing.focusVisibleOnly (a boolean) and focusRing.style, passed through from your design system's declared focus ring style. There is no width, colour or offset field of its own, and whether it returns anything for a given component depends on what your design system declared. If your resolved row declares an offset deliberately, that's correct to ship for your design system. Museum itself doesn't use one: the ring is a box-shadow, so an offset moves its effective radius away from the element's own corner, and an un-themed offset colour shows as a white halo in dark mode. Weigh that tradeoff for your own system rather than assuming offset is forbidden everywhere.

// Good: visible focus, no offset (Museum's declared ring shown)
className="focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/50"

// Bad: hidden focus
className="focus:outline-none"  // Without replacement

// Bad for Museum specifically: an offset moves the ring off the element's
// own radius, and Museum's un-themed offset colour is a white halo in dark
// mode. A design system that declares its own themed offset is not wrong to.
className="focus-visible:ring-1 focus-visible:ring-foreground/50 focus-visible:ring-offset-2"

Order rules

RuleRequirementWCAG
Logical tab orderMust follow visual / reading order2.4.3 (A)
No positive tabindexNever use tabindex="1", "2", etc.2.4.3 (A)
Skip links firstSkip links must be first focusable2.4.1 (A)
// Good
tabIndex="0"   // Focusable in natural order
tabIndex="-1"  // Programmatic focus only

// Bad
tabIndex="5"   // Creates unpredictable order
tabIndex="99"  // Maintenance nightmare

Focus trap rules

RuleRequirementWCAG
Modal focus trapFocus must cycle within modal2.4.3 (A)
Focus returnReturn focus to trigger on close2.4.3 (A)
Escape closesEscape should close overlays (except critical)Best practice

Landmark regions

Standard landmark regions for skip link navigation:

NameRoleHTML ElementSkip Link
mainmain<main>"Skip to main content"
navigationnavigation<nav>"Skip to navigation"
searchsearch<search>"Skip to search"
bannerbanner<header>:
contentinfocontentinfo<footer>:

MCP tool usage

Get focus pattern

// Get patterns for modal components
get_focus_pattern({ componentType: 'modal' })

// Get specific pattern
get_focus_pattern({ name: 'alert-dialog' })

// Include rules and landmarks
get_focus_pattern({
  componentType: 'modal',
  includeRules: true,
  includeLandmarks: true
})

Validate focus management

// Basic validation
validate_focus_management({
  code: '...'
})

// With component type context
validate_focus_management({
  code: '',
  componentType: 'dropdown'
})

// Strict mode (warnings become errors)
validate_focus_management({
  code: '...',
  strict: true
})

Validation checks

IssueSeverityFix
Missing focus indicatorErrorAdd the design system's declared focus ring (e.g. focus-visible:ring-1 focus-visible:ring-foreground/50), never focus: or a hand-picked ring-* value
Positive tabindexErrorUse tabindex="0" or -1
Modal without focus trapErrorUse focus trap library + aria-modal
Dialog missing aria-modalWarningAdd aria-modal="true"
Tab missing aria-selectedErrorAdd aria-selected attribute
Click without keyboardWarningAdd role="button" + keyboard handler
autoFocus usageWarningEnsure intentional and appropriate

Quick reference

Keyboard navigation by component

ComponentNavigationWrap?Activation
ModalTabTrapEnter / Space
MenuArrowYesEnter / Space
TabsArrowYesAuto or Enter
ToolbarArrowYesEnter / Space
ComboboxArrowNoEnter
TreeArrowNoEnter

Required ARIA by component

ComponentRequired attributes
Modalrole="dialog", aria-modal, aria-labelledby
Alert Dialogrole="alertdialog", aria-modal, aria-labelledby, aria-describedby
Menurole="menu", aria-labelledby
Menu Itemrole="menuitem"
Tabsrole="tablist", aria-orientation
Tabrole="tab", aria-selected, aria-controls
Tab Panelrole="tabpanel", aria-labelledby
Comboboxrole="combobox", aria-haspopup, aria-expanded, aria-controls

Decision guide

Building interactive component?
├── Is it an overlay?
│   ├── Modal / blocking? → Use focus trap + aria-modal
│   └── Non-modal? → No trap, but restore focus on close
├── Is it a list of options?
│   ├── Actions? → role="menu" + arrow keys + wrap
│   ├── Selection? → role="listbox" + arrow keys + no wrap
│   └── Navigation? → role="tablist" + arrow keys + wrap
├── Is it a button group?
│   └── Toolbar? → role="toolbar" + arrow keys + one tab stop
└── Is it page-level?
    └── Add skip links (first focusable element)

Was this page helpful?

Last reviewed by @jschuyler
All systems operationalDocsDevelopersPlatformPricing
PrivacyTerms© 2026 fndd, LLC