Focus Management
debt(d8/e3/b5/t7)
Closest to 'silent in production until users hit it' (d9), backing off to d8. The detection_hints explicitly state automated=no, and the tools listed (axe, nvda, jaws) require manual screen reader testing or specialist accessibility auditing — axe can flag some missing focus management but the code_pattern examples (SPA route changes, AJAX updates without focus moves) are largely invisible to automated tooling. Real breakage is only discovered when keyboard or screen reader users navigate the page, making this nearly silent in production.
Closest to 'simple parameterised fix' (e3). The quick_fix describes a targeted, repeatable pattern: after any dynamic content change, call .focus() on a container with tabindex='-1'. This is a small, localized fix per interaction point (modal open, route change, error display). While it must be applied in multiple locations across a SPA, each individual fix is a small code change rather than a cross-cutting architectural refactor.
Closest to 'persistent productivity tax' (b5). Focus management applies broadly across all SPA interactions — every modal, every route change, every dynamic content update must be considered. It doesn't rewrite the architecture but it is a persistent checklist that touches many work streams (routing, modal components, form validation, error handling). Applies to the web context broadly across the frontend codebase, imposing ongoing discipline on every feature team.
Closest to 'serious trap' (t7). The misconception field directly captures a strong trap: developers believe calling element.focus() is sufficient for SPA route changes, but focus must move to meaningful content (h1 or main) with an updated page title. This contradicts assumptions from traditional multi-page apps where the browser handles focus on navigation. The common_mistakes reinforce multiple non-obvious failure modes (focus escape from modals, missing focus restoration to trigger, tabindex='-1' without .focus() call) that contradict intuition.
Also Known As
TL;DR
Explanation
In static pages, browser focus follows natural tab order. In SPAs and dynamic UIs, focus management is the developer's responsibility. Key scenarios: modal opens (move focus to first interactive element), modal closes (return focus to trigger), route changes (move focus to main heading or skip-nav), inline errors appear (move focus to error summary), and notifications appear (announce via aria-live, not focus). Focus trapping in modals prevents tab from escaping into background content.
Diagram
flowchart TD
TRIGGER[User action opens modal] --> MODAL[Modal appears]
MODAL -->|move focus| FIRST[First focusable element in modal]
FIRST --> TRAP[Trap focus inside modal<br/>Tab cycles within]
TRAP --> CLOSE[User closes modal]
CLOSE -->|return focus| ORIGIN[Element that opened modal]
subgraph Focus_Indicators
VISIBLE[Visible focus ring<br/>never remove outline without replacement]
CONTRAST[High contrast indicator<br/>WCAG 3:1 minimum]
end
subgraph Common_Issues
SKIP[Focus jumps to background<br/>user lost in page]
LOST[Modal closes - focus goes to body<br/>not trigger element]
end
style FIRST fill:#238636,color:#fff
style ORIGIN fill:#238636,color:#fff
style SKIP fill:#f85149,color:#fff
style LOST fill:#f85149,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Not trapping focus in modals — Tab key escapes the modal into background content, making the modal unusable for keyboard users.
- Removing focus from modals on close without returning it to the trigger — users lose their place in the page.
- Using tabindex='-1' on the target element but not calling .focus() — the element is focusable programmatically but .focus() must be called.
- Focusing a non-interactive container div — focus must land on an element with a meaningful accessible name.
Code Examples
// Modal without focus management:
function openModal() {
document.getElementById('modal').style.display = 'block';
// Focus stays on the button that opened the modal
// Keyboard users cannot navigate inside the modal
// Tab escapes to the page behind the modal
}
// Modal with proper focus management:
function openModal(trigger) {
const modal = document.getElementById('modal');
modal.style.display = 'block';
modal.setAttribute('aria-modal', 'true');
// Move focus to first focusable element:
modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])').focus();
// Trap focus inside modal:
modal.addEventListener('keydown', trapFocus);
}
function closeModal() {
document.getElementById('modal').style.display = 'none';
trigger.focus(); // Return focus to the element that opened the modal
}