Skip to the content.

Focus management is the keyboard user's cursor

Focus management is the keyboard user's cursor

For someone using a mouse, the pointer is where their attention is. For someone using a keyboard or a screen reader, focus is that pointer — it is where they are on the page and where their next keystroke will land. Once you internalise “focus is the cursor,” a whole category of accessibility bugs becomes obvious: a dialog that opens but does not move focus is a cursor that did not follow the click; a menu you cannot Tab out of is a cursor stuck in a corner; a route change that leaves focus on the old link is a cursor teleported to nowhere. Managing focus deliberately is not a nicety — it is keeping the cursor where the user expects it.

Focus moves into a dialog on open and returns to the trigger on close A trigger button, then focus moving into an opened dialog's first control, then on close focus returning to the trigger. A dot traces the round trip. trigger open → focus in dialog (focus trapped) first control (focused) cancel close → focus returns to trigger
Open the dialog, focus moves in and is trapped; close it, focus returns to the trigger. The cursor makes a clean round trip and never gets lost.

Move focus when the context changes

When you open something new — a dialog, a drawer, a menu — move focus into it, or the keyboard user is left “behind” it, tabbing through the page underneath. The same applies to a single-page-app route change: the browser normally focuses the new document, but a client-side navigation does not, so the user’s cursor stays on the link they clicked while the whole page changes around it. Move it deliberately:

// on route change, move focus to the new page's heading so the cursor "arrives"
useEffect(() => {
  document.getElementById("page-heading")?.focus();
}, [pathname]);

Trap focus while a modal is open — then release it

While a modal is open, Tab should cycle within it, not escape to the page behind (which is inert to the eye but still reachable by keyboard). Trap focus on open, and — the half people forget — restore it to the trigger on close:

function useFocusTrap(ref, open) {
  const opener = useRef(null);
  useEffect(() => {
    if (!open) return;
    opener.current = document.activeElement;      // remember the trigger
    const focusables = ref.current.querySelectorAll("button, [href], input, [tabindex]");
    focusables[0]?.focus();                        // focus in
    return () => opener.current?.focus();          // focus back on close
  }, [open]);
}

A trap without the restore is its own bug: the user closes the dialog and their cursor is nowhere.

Never destroy focus, and keep it visible

Two failure modes finish the picture. First, do not remove or hide the focused element without moving focus somewhere sensible first — deleting the focused row of a list should move focus to the next row, not drop it to <body>, which yanks the cursor to the top of the page. Second, never do outline: none without a replacement: the focus ring is the visible cursor, and hiding it blinds keyboard users to their own position. Use :focus-visible so the ring shows for keyboard users without cluttering mouse clicks. Treat focus as the cursor it is — move it on context changes, trap and restore it in overlays, never destroy it, and always keep it visible — and your app becomes operable for people who never touch a mouse. The accessible-combobox and tabs-molecule exercises are built around exactly this focus choreography, which is where the “focus is the cursor” idea stops being a slogan and becomes code.