How do you make a custom component keyboard-navigable from scratch?

On this page

A custom component becomes keyboard-navigable when it satisfies four conditions at once: it is reachable in the tab order, operable with the keys users expect, it exposes its role and state to assistive technology, and it shows a visible focus indicator. Miss any one and the control is broken for someone navigating without a mouse. The reason this is real work is that native HTML elements give you all four for free, so the instant you replace a native control with styled markup, you inherit every accessibility duty that element used to handle.

Each condition rebuilds something the browser used to provide. Reachability means the element accepts focus, which for a non-native element requires tabindex set to 0 so it enters the natural tab sequence; tabindex of -1 makes it focusable only by script, and a positive value jumps the order in ways that almost always backfire, so 0 is the right default. Operability means wiring the keys that match the pattern: Enter and Space for a button, arrow keys to move within a group, Escape to dismiss an overlay. Role and state mean ARIA attributes that announce what the thing is and how it currently sits, such as a role of button or switch and an aria-expanded or aria-checked that updates as the user acts. The focus indicator means the focused state is visible at all times, which means never removing the browser outline without replacing it with something at least as clear. Together they reconstruct the contract a native control fulfills silently.

Picture a custom dropdown built from a styled div instead of a select. By sight it looks finished, but a keyboard user tabs straight past it because a div takes no focus, and even forced into focus it does nothing on Enter and announces nothing to a screen reader. Rebuilt correctly, the trigger has tabindex 0, opens on Enter or Space, carries role and aria-expanded that flips true when open, moves the highlight with the arrow keys, selects with Enter, closes with Escape, and shows a clear focus ring throughout. A second common case is the modal dialog assembled from a div: opened with a mouse it looks right, but focus never moves into it, Tab keeps cycling through the page behind it, and Escape does nothing, so a keyboard user is stranded. Done properly it takes focus on open, traps Tab inside until it closes, restores focus to the trigger afterward, and carries a dialog role with an accessible name. Same pixels in both, a working control instead of a decorative one.

The catch is the corollary of the rule: if you use the native element, you do not owe this work, because the platform already did it. A real button, anchor, input, or select arrives focusable, key-operable, role-bearing, and focus-visible out of the box. So the cheapest accessible component is almost always the native one lightly styled, and the from-scratch rebuild is justified only when no native element fits the interaction. The exception that makes the rebuild unavoidable is a genuinely composite pattern with no native equivalent, like a tablist, a tree, or a combobox with a custom popup, where the standard keyboard model is itself part of the spec you must implement rather than something you can borrow. Reaching for a div first and patching accessibility later is the expensive path, not the easy one.

Treat every custom control as a checklist before it ships: confirm it takes focus in tab order, responds to the keys its pattern implies, exposes an accurate role and live state through ARIA, and shows focus clearly. Test it with the keyboard alone, tabbing in and operating it without touching the mouse, and run a screen reader to hear what it announces. If any of the four fails, the control is not done, whatever it looks like.

Leave a comment

Your email address will not be published. Required fields are marked *