Ever tried tabbing through a custom dropdown or a fancy toggle switch you built, only to find the keyboard focus just skips over it, or worse, gets trapped inside with no way out? I’ve been there, frustrated by a UI that looked perfect but was a nightmare to navigate without a mouse.
Keyboard accessibility can easily slip through the cracks when you roll your own components. Native elements like <button> and <select> come with built-in keyboard behavior and focus handling baked in. Custom stuff? Not so much. You get the visuals, but the focus order, keyboard events, and screen reader cues? That’s all on you.
Let me walk you through the key challenges I ran into, how keyboard focus really works under the hood, and some concrete ways to implement and debug accessible keyboard navigation in custom UI components.
The focus trap: Why keyboard users get stuck or skipped
I once built a custom dropdown with divs and spans for styling freedom. It looked slick, but when I tested keyboard navigation, tabbing would jump right past it. Or sometimes the focus would enter the dropdown, but arrow keys didn’t move selection as expected. Worse, shift+tabbing out of the dropdown was impossible; the focus got stuck inside.
Why? Because by default, only certain elements are focusable , mostly interactive native elements like buttons and inputs. A div or span? Not focusable unless you add tabindex.
I had neglected to:
- Make the dropdown toggle button focusable with
tabindex="0"(or better, use a native<button>) - Manage keyboard events like ArrowUp, ArrowDown, Escape to open/close and navigate items
- Control focus movement on open to the first menu item
- Restore focus to the toggle when closing
Without explicit focus management, keyboard users either can’t reach the dropdown or get lost inside it.
How browser focus and tab order really work
Browsers assign a tab order based on the document’s focusable elements:
- Native focusable elements (
<button>,<input>,<a href>, etc.) come first - Elements with positive
tabindex(rarely recommended) - Elements with
tabindex=0(focusable in DOM order) - Elements with negative
tabindexare focusable programmatically but skipped in tab order
If your custom component’s parts lack tabindex=0 or aren’t native focusables, the keyboard will skip them.
Also, when you open something like a menu, you typically want to move focus into it programmatically (e.g., focus the first menu item). This prevents keyboard users from losing context.
Implementing keyboard support in custom components
Here’s a checklist I use when coding custom keyboard interactions:
-
Use semantic elements whenever possible. If your toggle behaves like a button, use
<button>. -
Make interactive elements focusable. Use
tabindex="0"on divs or spans if needed, but sparingly. -
Handle keyboard events explicitly. Listen for
keydownevents on your component, and respond to keys like Enter, Space, Arrow keys, Escape. - Manage focus on open/close. When a dropdown opens, move focus to the first item. When it closes, return focus to the toggle.
- Trap focus if needed. For modal dialogs or complex widgets, keep focus inside until the user closes or presses Escape.
-
Use ARIA roles and attributes properly.
role="menu",role="menuitem",aria-expanded,aria-haspopup, andaria-activedescendanthelp assistive tech understand your widget.
For example, for a custom dropdown:
<button
aria-haspopup="listbox"
aria-expanded={isOpen}
onClick={toggleOpen}
ref={toggleRef}
>
Select an option
</button>
{isOpen && (
<ul
role="listbox"
tabIndex={-1}
ref={listboxRef}
onKeyDown={handleKeyDown}
>
{options.map((option, i) => (
<li
key={option.id}
role="option"
tabIndex={focusedIndex === i ? 0 : -1}
onClick={() => selectOption(option)}
onFocus={() => setFocusedIndex(i)}
>
{option.label}
</li>
))}
</ul>
)}
In handleKeyDown, you’d manage ArrowUp/Down to move focusedIndex, Enter or Space to select, and Escape to close and return focus.
Debugging keyboard navigation woes
Testing keyboard accessibility is simple but powerful:
- Use Tab and Shift+Tab to navigate through your app’s interactive elements.
- Use screen reader tools to check announcements and focus highlights.
- Inspect the Accessibility Tree with browser devtools to verify roles and focusability.
If tab skips your component, check:
- Does the toggle have a native focusable element or
tabindex="0"? - Are interactive items focusable and in tab order?
- Are you accidentally using
tabindex="-1"where you want tab to land?
If focus gets trapped:
- Are you managing focus programmatically on open and close?
- Do you have a focus trap or modal code that might be overzealous?
If keyboard events don’t work:
- Are you listening for
keydowninstead ofkeypressorkeyup?keydownis best for handling navigation keys. - Are you calling
event.preventDefault()appropriately to avoid default scrolling or key behavior?
Browser devtools Accessibility pane lets you simulate keyboard focus and see the accessibility tree. Also, extensions like Axe or Lighthouse can flag common accessibility issues.
Beyond keyboard: Why accessibility is a team sport
Focus management is just one piece of accessibility. ARIA roles, visual focus indicators, announcements, and semantic HTML all matter. When you build custom UI, you’re responsible for recreating these affordances.
I learned that the best practice is to start with semantic HTML and enhance, rather than replace, native behavior. Use custom components only when necessary, and always test early and often with keyboard and assistive tech.
If you’re stuck, look at well-tested open source libraries like Reach UI or Radix UI. Their source code is a goldmine for how to handle focus and keyboard events right.
Final thoughts
Keyboard accessibility is not a checkbox , it’s a mindset. When you build a custom UI, imagine your app without a mouse. Walk through it with Tab. If you hit a blind spot, that’s where focus management needs work.
Fixing keyboard navigation bugs might feel tedious, but it makes your app usable by millions who rely on keyboards or assistive technology. And often, the fixes improve your code quality and UX for everyone.
So next time you craft a custom dropdown, toggle, or modal, don’t just make it look good , make sure you can tab through it cleanly, see where you are, and never get stuck.
Helpful learning resources
- W3C WAI accessibility guidance
- W3Schools accessibility tutorials
- MDN Web Docs Originally published at Under The Hood. Get the next deep dive in your inbox: subscribe to Under The Hood.
Top comments (0)