Skip to content

focus-management

Where focus goes after every open, close, delete, and route change.

When your agent loads it

Design where keyboard focus goes after every change in the interface — opening and closing dialogs, drawers, and menus; deleting, adding, and reordering items; route changes in single-page apps; form submission and validation errors; inline editing; dynamic and lazily loaded content; toasts; and composite widgets with roving focus — plus what gets announced to screen readers. Produces a focus map for each interaction. Use when building dialogs, menus, lists with delete, SPA navigation, multi-step forms, inline editors, or when keyboard or screen reader users lose their place. Triggers on "focus trap", "focus lost", "return focus", "keyboard navigation", "tab order", "focus after delete", "route change announcement", "aria-live". Not a full WCAG audit.

Group
Adaptation and input
Produces
Focus map
Length
142 lines
npx skills add aviralj02/interface-skills --skill focus-management

Installs only this skill. Add -g to install globally.

View source

PurposeLink to this section

Focus is the keyboard and screen reader user's position in the interface. Every time the DOM changes — a dialog opens, an item is deleted, a route changes — focus either moves somewhere intentional or falls back to <body> and the user is lost at the top of the page. This skill decides the destination for every change.

When to UseLink to this section

  • Building dialogs, drawers, popovers, menus, comboboxes, tabs
  • Lists where items can be deleted, added, moved, or filtered
  • Client-side routing
  • Forms with async submission or error summaries
  • Inline editing, expand/collapse, "load more", infinite scroll
  • Bug reports: "after closing the modal I'm at the top of the page", "screen reader didn't say anything"

Core PrinciplesLink to this section

  1. Every DOM change that removes or replaces the focused element needs a planned destination. Otherwise focus is lost.
  2. Move focus only when the user's context changed. Opening a dialog changes context; a toast appearing does not. Unrequested focus moves are disorienting.
  3. Return focus to where the user came from when a temporary context closes — or to the nearest logical substitute if that element is gone.
  4. Announce what the user cannot see. Changes that don't move focus but matter (results updated, saved, error) are announced through live regions.
  5. Visible focus is non-negotiable. A focus destination the user cannot see is not a destination.
  6. Tab order follows visual and logical order. Use DOM order; avoid positive tabindex.

WorkflowLink to this section

1. List the focus eventsLink to this section

For the feature, list every interaction that opens, closes, adds, removes, or replaces content.

2. Decide the destination for eachLink to this section

Reference rules:

EventFocus goes toAnnounce
Open modal dialogFirst meaningful element: the first input for forms; the dialog heading/container for content; the least destructive button for destructive confirmationsDialog role + label (automatic when labeled)
Close dialog (cancel/Escape/close)The element that opened it
Close dialog after actionThe element that opened it, or the result of the action if the trigger no longer exists (e.g. new item row)Result via live region if not visible from focus
Open drawer / non-modal panelPanel heading or first control if the user opened it to act; leave focus if it opens passively
Open menu / listboxFirst item (or selected item)
Close menu by selectingThe menu button (or the control whose value changed)
Delete item in a listNext item → previous item → list heading or empty state's action"'Q3 plan' deleted" + undo reachable
Add itemThe new item (or its first field if it needs input) — unless adding many in succession, then keep focus in the add field"Item added"
Reorder item (keyboard)Stays on the moved itemNew position: "Moved to position 3 of 8"
Filter / search results updateStays in the search fieldResult count: "12 results" (debounced)
Load moreFirst newly loaded item"20 more items loaded"
Infinite scrollUnchanged; provide a "load more" button or skip link as keyboard alternative
Route change (SPA)Page <h1> (with tabindex="-1") or main landmark; skip for in-page tab/query changesNew page title
Form submit — validation errorsError summary at top (with links to fields), or first invalid field for short formsError count / first error
Form submit — success, same pageSuccess message container, or the next logical controlSuccess message
Form submit — success, navigatesFollow route change rule; show confirmation on new pageConfirmation
Multi-step form next/backHeading of the new step"Step 2 of 4: Address"
Inline edit — enterThe input, with text selected if replacing is likely
Inline edit — save / cancelThe element that entered edit mode (the displayed value or edit button)"Saved" on save
Expand / collapse (disclosure)Stays on the toggleState via aria-expanded
Tab widgetArrow keys move between tabs; Tab moves into the panel
Toast / notification appearsUnchanged — never steal focusMessage via role="status" (or alert for errors); actions reachable via a known shortcut or landmark
Async content replaces focused element (e.g. skeleton → content)Equivalent element in the new content
Session expiry / re-auth dialogDialog; after re-auth, return to prior element with state intact

3. Handle "the return target is gone"Link to this section

The element that opened a dialog may no longer exist (it was the delete button of a row you just deleted). Define a fallback chain per case: next sibling → previous sibling → container heading → main heading. Never let it fall to <body>.

4. Contain focus where requiredLink to this section

  • Modal dialogs: focus cannot leave until closed; background is inert (inert attribute or native showModal()); Escape closes unless the dialog guards unsaved input.
  • Non-modal panels/popovers: focus can leave; Escape closes and returns focus; clicking outside closes without stealing focus from the clicked element.
  • Nested dialogs: avoid; if unavoidable, each level returns focus to its own trigger.

5. Composite widgets use roving focusLink to this section

Toolbars, menus, tab lists, listboxes, grids, and radio groups are a single Tab stop; arrow keys move within. Track the active item with roving tabindex or aria-activedescendant.

6. VerifyLink to this section

Walk every event with keyboard only, then with a screen reader (VoiceOver, NVDA). At each step: Where is focus? Can I see it? Did I hear what changed?

ChecklistLink to this section

  • Every open/close/add/delete/replace event has a focus destination
  • Dialogs return focus to their trigger, with a fallback if the trigger is gone
  • Modal dialogs contain focus and make the background inert
  • Destructive confirmations do not focus the destructive button first
  • Deleting an item moves focus to a neighbor or logical fallback
  • SPA route changes move focus to the page heading/main and announce the title
  • Form errors move focus to a summary or first invalid field
  • Toasts and background updates never steal focus, but are announced
  • Composite widgets use arrow-key navigation with a single Tab stop
  • Focus indicator visible on every focus destination (including tabindex="-1" headings)
  • No positive tabindex; DOM order matches visual order
  • Verified with keyboard and at least one screen reader

Common MistakesLink to this section

  • Custom modal built from divs without containment; Tab walks into the page behind.
  • Closing a dialog leaves focus on <body>, so the next Tab starts at the top of the page.
  • Deleting a row removes the focused button; focus is lost.
  • SPA navigation with no focus move, so screen reader users hear nothing and are still at the old link.
  • Autofocus on page load into a search field, skipping the page content and hijacking mobile keyboards.
  • Toasts with role="alert" for routine success, interrupting whatever the screen reader is reading.
  • outline: none with no replacement.
  • Live region added to the DOM at the same time as its message, so nothing is announced (the region must exist before content changes).

ExampleLink to this section

Focus map: task list with inline delete and undo

#EventFocus beforeFocus afterAnnouncement
1Press Delete on task 3 (R2, no confirm)task 3 "Delete" buttontask 4's title link (task 3 gone)"Task 'Draft brief' deleted. Undo available, press Ctrl+Z"
2Delete last task in listtask 8 "Delete"task 7's title linksame pattern
3Delete only remaining tasktask 1 "Delete"empty state's Add task button"Task deleted. No tasks left."
4Activate Undo in toasttoast Undo buttonrestored task's title link"Task restored"
5Press Ctrl+Z (shortcut)anywhere in listrestored task's title link"Task restored"

Implementation NotesLink to this section

  • Native <dialog> + showModal() provides focus containment, inert background, and Escape; you still set initial focus and return focus deliberately where the default is wrong.
  • The inert attribute makes background content unfocusable and hidden from assistive tech for custom overlays.
  • Store the trigger (document.activeElement) before opening; restore after closing, checking isConnected.
  • For headings that receive programmatic focus: tabindex="-1" and a visible :focus-visible style.
  • Keep a single persistent polite live region (and one assertive) mounted at the app root; write messages into it.
  • Framework routers: add a route-change hook that focuses the new <h1> and updates document.title.
  • After async DOM replacement, focus in requestAnimationFrame or after the framework commits, not synchronously before the element exists.

Output ExpectationsLink to this section

Produce:

  1. Focus event list for the feature.
  2. Focus mapEvent | Focus before | Focus after | Fallback | Announcement.
  3. Containment rules for each overlay.
  4. Live region plan — which messages, polite vs assertive.
  5. Findings — when reviewing, each lost/misplaced focus case with reproduction steps and file/line.