Accessibility in React: treating keyboard and focus flow as product behaviour
Accessibility is often treated as an end-of-development checklist. But when a person cannot complete a form by keyboard, loses focus in a dialog or cannot perceive an error message, the problem is not a missing label. It is a break in product behaviour.
State changes happen quickly in React interfaces: a modal opens, a list refreshes, a form error appears, a route changes or a menu closes. For keyboard and screen-reader users, focus, headings and announcements are the interface map.
Use built-in HTML behaviour first
Start by preserving HTML meaning. Using <button> for an action, <a> for navigation, <fieldset> and <legend> for related fields, and <table> for tabular data brings keyboard behaviour, focusability and screen-reader information without extra work.
Making a clickable div or span is not complete after adding onClick. Enter and Space keys, disabled state, focus indicator, role and accessible name must be recreated. Using a real button instead means less code and fewer errors.
Heading hierarchy should be separate from visual scale. When a page’s purpose is expressed with one h1 and subsections use headings in a logical order, a screen-reader user can move through the page quickly. Bold text chosen only for visual design does not provide this structure.
Test keyboard flow as a user journey
Navigating a page using only Tab and Shift+Tab reveals accessibility problems quickly. Is focus visible? Does order match visual and functional flow? Does keyboard focus stop on invisible but clickable elements? Which keys work when a menu opens? These questions show whether a component is actually usable.
Focus order follows DOM order. Moving an item on screen with CSS while leaving it elsewhere in the DOM can make keyboard travel confusing. Build a meaningful DOM order rather than repairing it with positive tabindex values. tabindex="0" should also be reserved for a genuinely interactive element that is not naturally focusable.
Do not remove a focus indicator for design. If the browser default does not fit the visual language, create a clearer alternative, but preserve the ability to see which element has focus.
Manage focus in modals and layered surfaces
When a modal opens, move focus to a meaningful item that communicates its beginning. The modal heading or first usable control is usually appropriate. While it is open, Tab must not escape to the page behind it; when it closes, focus should return to its trigger.
This behaviour is not independent of React state changes. Does the trigger still exist when a modal closes? Is it disabled? Is the person closing because of a form error? These details need design. Starting with a ready, accessibility-tested dialog component is usually safer than manually building a focus trap.
Smaller surfaces such as menus, popovers and autocomplete controls carry the same question: how does keyboard input enter after opening, move between options, use Escape and return focus? Not every surface is modal; locking background interaction unnecessarily can also harm experience.
Announce meaningful state changes
A small toast at the bottom of the screen after a form submission may not provide enough feedback to a screen-reader user. Announce success, error or loading outcomes through an appropriate live region, without creating a constant stream of unnecessary messages.
In form validation, an error must be more than a red border. The field label, error text and explanation should be programmatically associated. If submission fails, move focus to the first invalid field or an error summary. When links in that summary jump to their fields, recovery is easier in long forms.
When old content stays visible during asynchronous loading, do not interrupt a person with needless announcements. Prioritise states that actually change content, create an error or report completion. Instead of simply saying “failed”, an announcement should explain what failed and the next safe action.
Apply a keyboard model to custom controls
Tabs, comboboxes, tree views and drag-and-drop areas are more complex than standard HTML controls. Adding only an ARIA role is not enough; expected keyboard interaction for that role must be implemented. Arrow keys, Home/End, Enter, Space and Escape should work consistently for the component type.
When complexity is not needed, choosing a simpler component is more accessible. A correctly labelled <select> can be enough for a filter instead of a fully custom combobox. When a custom control is necessary, starting with an accessible component library and defining shared project rules reduces repeated mistakes.
Preserve context on route changes
In single-page applications, visual content can change while browser focus remains at its old position. Moving focus deliberately to the new page heading or main content tells the person that navigation completed. Doing this for every minor filter change can be disruptive; limiting it to true page-level transitions is more balanced.
A skip-to-content link also gives keyboard users a quick way past repeated navigation. It should be one of the first focusable elements and move clearly to the main content.
Move testing from automation to human behaviour
Automated accessibility checks catch many issues early, such as missing accessible names or invalid ARIA. They cannot alone prove that focus order is meaningful, a modal is usable or an error message is understandable.
Create a short manual scenario for every critical flow: sign in with a keyboard, change a filter, submit a form with errors, open and close a modal, change a route. Periodically trying these with a screen reader and different devices exposes issues between components. Tests should interact through real user actions where possible: clicking, typing, Tab and Escape.
Accessibility is not an extra feature for a separate group. Semantic structure, predictable focus, clear feedback and keyboard support mean fewer errors, better navigation and a more resilient React interface for everyone.