JavaScript Focus Management Table

PieceWhat it doesField note
el.focus({ preventScroll })Focus without jumpThe scroll jump killer - modal open without page teleport
:focus-visibleKeyboard-only ringsMouse clicks no ring, Tab does - the browser's heuristic, now CSS
focus-trap patternTab stays insideModals trap Tab - on last el, Tab wraps to first
inert attributeSubtree un-focusableBehind-the-modal content INERT - focus cannot even land
tabindex=0 vs -1Join vs skip0 = tabbable; -1 = programmatically only - panels get -1
Autofocus the right elFirst INPUT not buttonDialogs autofocus the first interactive - content dictates
focusout/focusinBubbling focus eventsfocus doesn't bubble; focusout/in do - delegation works
Return focus on closeThe closing courtesyStore opener, restore on close - where the user LEFT
Reference: the MDN focus reference. Focus management is keyboard accessibility's core: focus({ preventScroll: true }) opens modals without teleporting the page, :focus-visible gives keyboard users their rings while sparing mouse clicks, the focus-trap pattern keeps Tab inside modals, and the inert attribute makes everything behind a modal un-focusable natively. Bottom line: store the opener and restore focus on close (where the user LEFT), autofocus the first input not the button, and remember focus does not bubble - focusout/in do, which is why delegation uses those. Related tools: WAI-ARIA table (the semantics around focus), border table (outline vs border for rings), and popover table (the native focus handling).

Focus management is keyboard accessibility's core: focus({ preventScroll: true }) opens modals without teleporting the page, :focus-visible gives keyboard users their rings while sparing mouse clicks, the focus-trap pattern keeps Tab inside modals, and the inert attribute makes everything behind a modal natively un-focusable.

Bottom line: store the opener and restore focus on close (where the user LEFT), autofocus the first input rather than the button, and remember focus does not bubble - focusout/focusin do, which is why event delegation uses those.

The honest part: every custom widget with focus management is re-implementing what native elements ship free - the moment a focus trap or manual tabindex appears, ask whether a dialog, details or button could carry the semantics instead.

How to use

  1. Open without teleporting: modal.show(); previouslyFocused = document.activeElement; firstInput.focus({ preventScroll: true }) - and restore on close.
  2. Trap Tab in modals: on keydown Tab, if focus would leave the modal, move it to the first (Shift: last) focusable - or use inert on the background.
  3. Make everything behind a modal inert: main.inert = true - focus, clicks and screen-reader reach all skip it, natively.

Frequently asked questions

Why does focus({ preventScroll: true }) exist?

Because focusing SCROLLS by default: calling focus() on an offscreen element scrolls the page to reveal it - usually desirable, catastrophic on modal open (the page behind teleports to wherever the first input lives, and the user loses their reading position). preventScroll focuses the element in place. The companion discipline: when you prevent the scroll, YOU own making sure the focused element is actually in view - for modals that is trivial (they overlay the viewport); for inline reveals it means scrolling the container, not the page, yourself.

What does :focus-visible solve that :focus could not?

The mouse-click ring problem. :focus applies on ANY focus - including mouse clicks on buttons, which produced the infamous ring on click that designers kept deleting, which in turn destroyed keyboard users' only indicator. :focus-visible applies the ring only when the browser HEURISTIC thinks keyboard focus indication helps: Tab navigation shows it, mouse clicks do not. The result: keep the visible indicator, lose the click ring, satisfy WCAG 2.4.7 - one pseudo-class resolving the designer-versus-a11y war. Never remove focus indicators entirely; restyle them to fit the design if needed.

How does the focus-trap pattern work, and when is it required?

Tab is a cycle; the trap contains the cycle. In a modal, Tabbing past the last focusable element would escape to the page behind (invisible and unreachable) - the trap listens for Tab on the modal's last focusable and moves focus to the first, and Shift+Tab on the first moves to the last. It is REQUIRED for modal dialogs: keyboard users must not be able to Tab into content they cannot see or reach. The modern simplifications: the inert attribute on the background makes everything outside the modal unfocusable natively (Tab wraps automatically - nothing outside accepts focus), and the Popover API and dialog element handle the trap internally. Custom traps are for custom widget hostilities.

Why does returning focus on close matter so much?

Continuity of place. A keyboard user Tabbed to the 'Delete' button, hit Enter, confirmed in a dialog - and focus vanished to document.body: their place in the page is gone, and the next Tab starts from the top (or the browser re-inserts them somewhere random). The courtesy: store document.activeElement when the dialog opens, restore .focus() on close - the user continues where their action left them, with the deleted row's neighbor now focused. This is in the WAI-ARIA dialog pattern explicitly, and its absence is the most common keyboard-accessibility bug report in component libraries.

Related tools