Ever had that moment where you open a modal or dropdown, start tabbing through, and suddenly the keyboard focus vanishes into thin air? Or worse, you get stuck inside a widget with no way out without a mouse?
I’ve been there, frustrated and scratching my head, especially when these bugs crop up in polished design systems you’d expect to handle this smoothly.
Focus management is one of those invisible, tricky parts of UI that make or break accessibility. Behind the scenes, design systems invest surprisingly complex logic to make sure keyboard users never lose their spot or get trapped.
Let me walk you through what I learned digging into how popular design systems manage focus internally, the kinds of focus trap bugs that still sneak in, and how you can debug these issues in your own complex components.
Why Focus Management Is Harder Than It Looks
On the surface, managing focus sounds simple: when a user tabs, move focus to the next logical element. When a modal opens, focus the first input. When it closes, return focus back.
But under the hood, several tricky things happen:
- Focus loss: When you remove or hide elements, the browser may move focus unexpectedly or nowhere at all.
- Focus traps: Modals and popovers often want to trap focus inside, so keyboard users can’t tab out accidentally. But implementing this trap correctly is subtle.
- Nested components: Complex component trees with nested modals, popovers, dropdowns, and tooltips can confuse the browser’s native focus order.
- Dynamic DOM changes: Reactivity, animations, and mounting/unmounting components can cause focus to jump or disappear.
The result? Bugs like keyboard users getting stuck inside a modal, focus jumping unpredictably, or focus disappearing completely.
Peek Under the Hood: How Design Systems Manage Focus
I looked into a few popular design systems , think Material UI, Chakra UI, and Reach UI , and here’s how they tackle the problem.
1. Focus Scope and Focus Trap
Most systems create a "focus trap" around modals and popovers. This means:
- They listen to keyboard events, especially Tab and Shift+Tab.
- When the user tabs forward from the last focusable element inside the trap, they move focus back to the first.
- When the user tabs backward from the first element, they move focus to the last.
This cyclical focus prevents the keyboard from escaping the modal unintentionally.
Underneath, they either:
- Use invisible sentinel elements (focus sentinels) before and after the trap to catch tab events.
- Or listen to
keydown events and manually call event.preventDefault() and focus() to redirect.
Here’s a simplified example:
function trapFocus(container) {
const focusableElements = getFocusableElements(container);
container.addEventListener('keydown', e => {
if (e.key !== 'Tab') return;
const focusedIndex = focusableElements.indexOf(document.activeElement);
if (e.shiftKey && focusedIndex === 0) {
e.preventDefault();
focusableElements[focusableElements.length - 1].focus();
} else if (!e.shiftKey && focusedIndex === focusableElements.length - 1) {
e.preventDefault();
focusableElements[0].focus();
}
});
}
2. Restoring Focus on Close
Good design systems save the currently focused element before opening a modal or popover and restore focus to it when the overlay closes.
This seems straightforward but can get tricky when the original element unmounts or the DOM changes.
3. Managing Focus on Mount
When a modal opens, the ideal is to focus the first interactive element inside. But what if there’s no focusable element? Or the element appears after an animation?
Some systems wait until the content is fully rendered or use a setTimeout to delay focusing.
Others allow you to specify which element should receive focus.
4. Handling Nested Focus Traps
What if you have a modal inside a modal? Or a dropdown inside a popover?
Design systems keep track of nested traps using stacks. The most recently opened trap is active, while others are paused or disabled.
This ensures keyboard navigation respects the innermost overlay first.
Common Focus Trap Bugs and What Causes Them
Despite best efforts, focus bugs still happen, especially in custom or complex UIs.
Here are some patterns I’ve seen:
The Phantom Focus Bug
Focus disappears completely. Keyboard users tab, but nothing highlights.
Usually caused by:
- The focused element being removed or hidden without focus moving elsewhere.
- Focus being moved to a non-focusable container.
- Timing issues where focus is set before the element is in the DOM.
The Focus Loop Break
Tabbing forward or backward escapes the trap unexpectedly.
Often caused by:
- Missing or broken sentinel elements.
- Multiple event listeners fighting over focus.
- Nested traps not properly prioritized.
The Focus Trap Lock
Focus gets stuck inside a component with no way out.
Caused by:
- Failing to restore focus to the element that opened the trap.
- Keyboard handlers swallowing Tab or Shift+Tab but not redirecting focus.
- Overly aggressive focus locking when multiple overlays are open.
Debugging Focus Issues in Complex Trees
When your UI has nested components, portals, and lots of dynamic elements, debugging focus gets tricky.
Here’s a practical approach:
1. Visualize Focus
Use Chrome DevTools or Firefox to inspect the focused element:
- In DevTools console,
document.activeElement tells you where focus currently is.
- Add a global CSS style like
*:focus { outline: 2px solid hotpink !important; } to clearly see focus.
2. Log Focus Events
Listen for focusin and focusout on the document to track focus movement:
document.addEventListener('focusin', e => console.log('Focus in:', e.target));
document.addEventListener('focusout', e => console.log('Focus out:', e.target));
This helps catch unexpected focus jumps or losses.
3. Check Focusable Elements
Run a quick helper to list focusable elements inside your trap:
function getFocusableElements(container) {
return [...container.querySelectorAll(
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
)];
}
Make sure you actually have focusable elements where you expect.
4. Inspect Event Listeners
Use DevTools to see which event listeners are attached to trap elements. Conflicting listeners can break focus management.
5. Test Keyboard Navigation Manually
Slowly tab through your UI and watch focus move. Try shift+tab. Notice where focus gets stuck or lost.
6. Look for Portals and DOM Moves
Elements rendered via portals or outside the normal DOM flow can confuse focus. Make sure your focus trap accounts for that.
A Real-World Example: Debugging a Focus Trap Bug in a Modal
I recently helped debug a modal that sometimes lost focus after opening.
The symptoms:
- Open modal
- Keyboard focus briefly appeared on the first button
- Focus disappeared and tabbing did nothing
After some digging:
- The modal content was rendered asynchronously with a slight delay.
- The focus trap attempted to focus the button immediately on open.
- Because the button wasn’t in the DOM yet, focus landed nowhere.
The fix:
- Delay the initial focus call until after the modal content mounted.
- Use a
useEffect hook with dependencies on modal open state.
useEffect(() => {
if (isOpen) {
const timer = setTimeout(() => {
firstButtonRef.current?.focus();
}, 0);
return () => clearTimeout(timer);
}
}, [isOpen]);
That tiny delay made all the difference.
Wrapping Up
Focus management is a delicate balancing act hidden under the hood of every accessible UI component. Design systems invest a lot of subtle logic to keep keyboard users on track, avoid traps, and restore focus correctly.
If you build custom components or integrate third-party design systems, understanding these mechanisms helps you avoid nasty focus bugs and debug them faster when they do appear.
Keep an eye on:
- Focus traps and how Tab/Shift+Tab keys behave
- Focus restoration after overlays close
- Timing issues with dynamic rendering
- Nested traps and portals
And don’t forget to test keyboard navigation as early and often as you can.
Your keyboard users will thank you.