A team ships a command palette, or a set of global keyboard shortcuts, and it works fine in every manual test. Then real usage starts and the bug reports come in: pressing a shortcut does nothing, or does the wrong thing, or interrupts someone mid-sentence in a text field. Nine times out of ten, the root cause isn't the shortcut logic itself. It's focus management, and it's the part of keyboard interface design that gets the least attention relative to how often it breaks things.
The Symptom Looks Like a Shortcut Bug, But Isn't
The classic failure mode: a user opens a command palette with Cmd+K, selects an action, and the palette closes. But focus doesn't go anywhere sensible. It falls back to document.body, or it stays on whatever DOM node happened to be focused inside the now-removed palette component. The next time the user presses a keyboard shortcut, expecting it to act on the page they were just looking at, nothing happens, because focus is sitting somewhere invisible and unhelpful.
Reported as a bug, this looks like "shortcuts stopped working." Debugged properly, it's almost always a missing element.focus() call at the point where a modal or overlay component unmounts. The shortcut logic was never broken. The thing listening for keyboard events lost track of where the user's attention actually was.
This distinction matters for how a team should triage the bug report in the first place. "Shortcuts stopped working" sends an engineer looking at event listeners, key codes, and modifier key detection, none of which is where the actual defect lives. The fix is almost never in the shortcut handler itself; it's in whatever component opened and closed without telling the browser where focus should go next.
Where the Browser's Default Focus Behavior Isn't Enough
The DOM's default focus behavior handles simple cases fine: click a button, it gets focus; tab through a form, focus moves predictably between fields. It has no opinion at all about what should happen when a modal, dropdown, or command palette opens and closes on top of the existing page. That's application logic your code has to own explicitly.
function openPalette() {
const previouslyFocused = document.activeElement;
paletteInputRef.current.focus();
return function closePalette() {
paletteRef.current.remove();
if (previouslyFocused && document.contains(previouslyFocused)) {
previouslyFocused.focus();
}
};
}
This pattern, capture what had focus before the overlay opened and restore it on close, is the single most important piece of focus management for any transient UI element. Skip it and every overlay in your app becomes a small trap that quietly resets the user's position every time they use it.
Focus Trapping Inside Modals and Palettes
The inverse problem is focus escaping where it shouldn't. If a command palette is open and the user presses Tab, focus should cycle through the elements inside the palette, not escape into the page behind it. Without an explicit focus trap, Tab can move focus to a link or button the user can't currently see, because it's visually behind the modal overlay, which is disorienting for sighted mouse users and actively broken for keyboard and screen reader users who navigate entirely by focus order.
A minimal focus trap listens for Tab and Shift+Tab and wraps focus back to the first or last focusable element inside the container:
function trapFocus(container, event) {
const focusable = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.key !== "Tab") return;
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
This is a well-understood pattern with edge cases that are easy to get subtly wrong (dynamically added content changing the focusable set, nested modals, elements that are focusable but hidden). The W3C's ARIA Authoring Practices Guide documents the expected focus behavior for modal dialogs in detail, including how focus trapping interacts with aria-modal and screen reader announcement, and it's worth building against that reference rather than guessing at the edge cases from scratch.
Global Shortcuts and Focus Context Have to Agree
Global keyboard shortcuts and focus management are really the same problem viewed from two angles. A global shortcut handler that doesn't check focus context will fire inside text inputs, code editors, and other places it shouldn't. The fix is checking document.activeElement before executing a global handler, but that check is only meaningful if focus itself is being tracked and restored correctly elsewhere in the app. A shortcut system built on top of unreliable focus tracking inherits every focus bug as a shortcut bug, because the shortcut handler's understanding of "where is the user right now" is only as good as the focus state underneath it.
document.addEventListener("keydown", (event) => {
if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
openCommandPalette();
}
});
That five-line handler looks trivial, but it depends entirely on openCommandPalette correctly managing focus capture and restoration, and on every other overlay in the app not leaving focus in a broken state that makes the next shortcut behave unpredictably.
This is why focus bugs tend to compound rather than stay isolated. A single unmanaged overlay component might cause one confusing interaction. Three or four of them in the same app, each with slightly different focus handling, produce a system where keyboard behavior feels different depending on which sequence of overlays a user happened to open and close, and no single shortcut handler is individually "wrong" enough to point at as the cause.
Screen Readers Depend on Focus Even More Than Sighted Users Do
For a sighted mouse user, a focus bug is an annoyance: they can see where things are and click to correct it. For a screen reader user, focus is the entire interface. If focus lands somewhere unexpected after a shortcut fires, or fails to move at all when a palette opens, a screen reader has nothing meaningful to announce. The user has no visual layout to fall back on, so a silent focus bug isn't a minor inconvenience, it's the interface going effectively blank.
WebAIM, a long-running accessibility research and testing organization, publishes detailed guidance on how focus order and focus visibility interact with screen reader announcements, and it's a useful reference for understanding why focus management bugs disproportionately affect the users who are hardest to reach through casual manual testing. A sighted developer clicking through a feature will rarely notice a focus bug that makes the feature effectively unusable for someone navigating by keyboard and screen reader alone.

Photo by Jessica Lewis 🦋 thepaintedsquare on Pexels
Testing Focus, Not Just Shortcut Behavior
Most teams test keyboard shortcuts by checking that the right action fires. Fewer teams test where focus ends up afterward, which is exactly the part that breaks in production. A useful addition to an existing test suite: after triggering a shortcut and closing whatever it opened, assert that document.activeElement matches what it was before the interaction started, not document.body and not some orphaned node from a removed component.
This kind of test catches the exact class of bug described at the top of this piece, the one that gets reported as "shortcuts randomly stop working" and takes an afternoon to trace back to a missing focus restoration call. Testing it explicitly, rather than relying on manual spot checks, turns an intermittent production bug into a caught regression before it ships.
test("focus returns to trigger button after palette closes", () => {
const trigger = screen.getByRole("button", { name: /open command palette/i });
trigger.focus();
fireEvent.keyDown(document, { key: "k", metaKey: true });
fireEvent.keyDown(document, { key: "Escape" });
expect(document.activeElement).toBe(trigger);
});
Testing utilities built around accessible queries, like the Testing Library family of tools, make this kind of focus assertion straightforward to write because they encourage querying the DOM the way a real user or screen reader would, by role and accessible name, rather than by implementation-specific selectors that break the moment a component's internals change.
137Foundry wrote a longer guide covering the full interaction design behind a command palette, including the interaction model, fuzzy search, and accessibility considerations beyond focus management specifically, for teams building a keyboard-driven interface from the ground up rather than debugging one already in production.
The Fix Is Almost Always the Same Shape
Whether the symptom is a shortcut that stops responding, a modal that traps focus incorrectly, or Tab escaping where it shouldn't, the underlying fix is nearly always the same: capture focus state explicitly before a UI change, restore or redirect it explicitly after, and never assume the browser's default behavior will do the sensible thing on your behalf. The DOM gives you the primitives (focus(), activeElement, tabindex) but none of the policy. Writing that policy down once, as a small set of reusable focus management utilities, saves a team from re-debugging the same class of bug in every new overlay component they ship.
Top comments (0)