Ever been puzzled when tabbing through a complex UI built from a design system, only to have the focus jump in unexpected ways or disappear entirely? Maybe a modal’s close button isn’t reachable by keyboard, or focus lands on something visually hidden. It’s a tiny annoyance for some, but a showstopper for keyboard users and screen reader folks.
I’ve faced this exact headache while working on a large React design system. The components promised accessibility out of the box, but subtle app-level overrides or custom wrappers sometimes broke the focus flow. Tracking down where the focus management logic lived, and why it wasn’t working, led me down a rabbit hole of internal mechanisms that design systems use to enforce consistent, accessible focus.
Let me share what I learned about how design systems coordinate focus, how they integrate with ARIA, and how you can debug when your app’s overrides clash with those defaults.
Focus management is more than tabindex
Setting tabindex is the classic way to control keyboard focus order. But design systems go way beyond sprinkling tabindex attributes.
For example, take a dropdown component. When you open it, focus should move into the dropdown’s first interactive item. When you close it, focus needs to return to the button that triggered it. And keyboard navigation inside the dropdown needs to cycle logically.
Design systems bake these rules into their components using JavaScript focus management. They listen for keyboard events, programmatically call .focus() on appropriate elements, and maintain internal state about what should be focused next.
This means a lot of logic lives inside the system’s components, not just in markup.
How design systems enforce consistent focus
1. Centralized focus utilities
Many systems include shared focus utilities: helper functions or hooks that set focus, trap focus within dialogs, or restore focus after navigation. These utilities handle quirks across browsers and assistive tech.
For example, a "focus trap" utility listens for tab key presses and ensures focus cycles only within a modal’s interactive elements, preventing keyboard users from tabbing to elements behind the modal.
2. Focus context and state
Some systems create a focus context that tracks the current focus target and allows nested components to coordinate. For example, a tab panel component and its tabs share focus state so that activating a tab moves focus correctly and updates the panel.
3. ARIA integration
ARIA attributes are part of the story but not the whole story. Design systems use ARIA roles like role="dialog", aria-modal="true", and aria-labelledby to help screen readers understand component purpose and relationships.
But focus management is still done with JavaScript. For instance, an accessible modal will set aria-hidden="true" on background content and shift focus to the modal container when opened.
4. Keyboard event handling
Design systems intercept keyboard events like Tab, Shift+Tab, Escape, and arrow keys to customize focus movement and component behavior.
For example, arrow keys might move focus between menu items, Escape closes a modal and returns focus, and Tab cycles focus inside a focus trap.
When your app overrides break focus management
I ran into bugs when app-level styles or wrappers overrode the design system’s components. Here are common pitfalls:
-
Custom wrappers missing focus forwarding: If you wrap a focusable component but don’t forward refs properly, programmatic .focus() calls inside the system fail silently.
-
CSS hiding focus indicators: Overriding focus styles with outline: none or other rules can make focus invisible.
-
Conflicting event handlers: Adding keyboard handlers that prevent default behavior or stop propagation can break the system’s internal focus logic.
-
Dynamic rendering mismatches: Rendering components conditionally without coordinating focus restoration leads to lost focus or unexpected jumps.
Debugging focus issues in design systems
Use browser devtools’ accessibility inspectors
Browsers like Chrome and Firefox have accessibility panes showing the accessibility tree and focusable elements. Use these to verify which elements are focusable and whether ARIA attributes are set correctly.
Manual focus inspection
Try tabbing through your app step by step. Note where focus lands, where it disappears, or where it loops unexpectedly.
Use document.activeElement in the console to see the current focused element at any point.
Check event listeners
Inspect the event listeners on elements to see if keyboard events are handled or blocked.
Verify ref forwarding
If your components use React, ensure ref forwarding is correctly set up so the design system can call .focus() on the right DOM node.
Test with screen readers
Keyboard focus and visual focus indicators are only part of accessibility. Test with screen readers like NVDA or VoiceOver to make sure focus changes align with announcements.
Example: Focus trap in a modal
Here’s a simplified example of how a focus trap works inside a modal component:
function Modal({isOpen, onClose, children}) {
const modalRef = useRef(null);
useEffect(() => {
if (!isOpen) return;
const focusableElements = modalRef.current.querySelectorAll(
'a[href], button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
function handleKeyDown(e) {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
if (e.key === 'Escape') {
onClose();
}
}
modalRef.current.addEventListener('keydown', handleKeyDown);
firstElement.focus();
return () => {
modalRef.current.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div role="dialog" aria-modal="true" ref={modalRef}>
{children}
</div>
);
}
This snippet traps focus inside the modal while it’s open and closes on Escape. Design systems build on this idea but handle edge cases and browser quirks.
Wrapping up
Focus management inside design systems is a mix of ARIA, JavaScript, event handling, and CSS working together. It’s not enough to add tabindex and call it done.
If you build or maintain a design system, consider:
- Providing standardized focus utilities
- Documenting expected focus behavior for each component
- Testing with keyboard and screen readers
- Encouraging app teams to respect focus management and avoid harmful overrides
If you’re integrating a design system and hit focus bugs, dig into the event handling, ref forwarding, and CSS focus styles. Use browser devtools and assistive tech to observe what’s happening.
These mechanisms might feel like plumbing, but getting focus right makes your UI genuinely usable for everyone.