Keyboard Navigation in React Components: Patterns for Accessibility

When building React apps, it’s easy to focus on how components look or behave for most users, but sometimes we forget about those who navigate with a keyboard, rather than a mouse. Keyboard users, whether they rely on assistive technology or just prefer keys over clicks, can run into frustrating barriers when tab order, focus, or interactive patterns aren’t handled properly.

Today, I’ll walk you through practical patterns to make your React components fully keyboard-accessible. First, we’ll cover the most common accessibility issues when it comes to navigating through a page using a keyboard, and I'll explain how to prevent them. By the end, you’ll have concrete, production-ready strategies to make your interfaces more inclusive.

Common Keyboard Accessibility Issues

Incorrect Order of Elements

One of the most common accessibility issues in React applications is an unexpected or illogical order of elements. Keyboard users rely on predictable navigation: pressing Tab should move them through interactive elements like inputs, buttons, and links in the order that matches the visual layout and the intended workflow. However, if the JSX structure doesn’t reflect the actual visual flow of the UI, the browser’s default tab order won’t match what users see on the screen. This creates a confusing and frustrating experience, especially in forms.

Consider the following form design:

An example of simple form with fields and submit button

The visual layout suggests that users should first fill in their first and last name, then their email, company name and finally submit the form. But depending on how the JSX is structured, the browser may focus elements in a completely different order. For example, the markup below looks fine at a glance but results in an incorrect tab order:

	
function IncorrectForm() {
  return (
    <>
      <form style={{ display: 'flex' }}>
        <div style={{ display: 'flex', flexDirection: 'column' }}>
          <input type="text" placeholder="First Name" />
          <input type="email" placeholder="Email" />
          <button type="submit" style={{ width: 'fit-content' }}>
            Sign Up
          </button>
        </div>
        <div style={{ display: 'flex', flexDirection: 'column' }}>
          <input type="text" placeholder="Last Name" />
          <input type="text" placeholder="Company Name (optional)" />
        </div>
      </form>
    </>
  );
}

Visually, the email and company name may appear above the button depending on your CSS grid, flexbox, or responsive layout. But the DOM order still dictates the keyboard order. If your CSS moves elements around without explicitly adjusting their order, the tabbing sequence will no longer follow the actual layout that users see. This mismatch is exactly what you want to avoid.

Gif of incorrect form order

To ensure proper keyboard navigation, the DOM structure should always follow the logical order of the form, regardless of how the UI is visually styled.

In this case, the correct structure places the fields in the exact sequence users should complete them: first name, last name, email, company name, and finally the submit button. With this order reflected directly in the JSX, the browser’s natural tab flow aligns perfectly with the visual layout, making the form intuitive for both keyboard and screen-reader users.

	
function CorrectForm() {
  return (
    <>
      <form>
        <div>
          <div>
            <input type="text" placeholder="First Name" />
            <input type="text" placeholder="Last Name" />
          </div>
          <div>
            <input type="email" placeholder="Email" />
            <input type="text" placeholder="Company Name (optional)" />
          </div>
        </div>
        <button type="submit">Sign Up</button>
      </form>
    </>
  );
}
    
Gif of correct form order

Losing Focus / Escape Issues

Another common accessibility challenge in React components is unintended focus loss: situations where keyboard users suddenly lose their place because the focus jumps to the wrong element, disappears entirely, or becomes trapped in the wrong part of the UI. This typically happens with components that dynamically appear or disappear, such as modals, dropdowns, accordions, or popovers.

A classic example is a modal that opens but doesn’t correctly manage focus. Ideally, when the modal appears, focus should move to the first interactive element inside it (for example, a close button or a form field). When users press Escape, the modal should close and return focus back to the trigger element. But without proper handling, one of these issues happens:

  • The modal opens but focus stays behind on the main page
  • Focus jumps to the browser’s address bar or disappears completely
  • Keyboard Users close the modal with Escape, but focus doesn’t return to where they left off
  • Keyboard users can tab outside of the modal entirely

Dropdowns behave similarly. If the dropdown closes (for example, when the user presses Escape or tabs away), focus should return to the button that opened it. Failing to restore focus leaves users disoriented—they can't tell where they are or how to continue navigating.

The example below shows what happens when a dropdown does not handle focus correctly:

Gif of inaccessible dropdown

Notice how the user loses their place as soon as the dropdown opens, nothing happens, the user can't go through the options and on Escape the focus is not returned to the button that triggers the dropdown.

The following example demonstrates proper focus management: the keyboard focus moves into the dropdown when it opens and returns to the trigger when it closes, keeping the user oriented and in control.

Gif of accessible dropdown

Missing ARIA Attributes

Another frequent accessibility issue in React components is the absence of ARIA (Accessible Rich Internet Applications) attributes. ARIA attributes provide semantic information to assistive technologies, like screen readers about the role, state, or properties of an element. Without them, dynamic components such as modals, accordions, tabs, and dropdowns can be confusing or unusable for keyboard and screen-reader users.

For example, a modal without role="dialog" or aria-modal="true" might not be announced as a dialog, leaving users unaware that a new interaction context has appeared. Similarly, a button that expands a collapsible section without aria-expanded fails to inform the user whether the section is open or closed.

How to Prevent Keyboard Accessibility Issues

Preventing keyboard accessibility problems involves three key strategies: ordering elements correctly, managing focus, and using ARIA attributes. Applying these practices ensures that your React components are intuitive, predictable, and usable for all keyboard users.

Order Elements Correctly

The first step in preventing keyboard accessibility issues is ensuring that the order of elements in your JSX matches the logical flow of the UI. Keyboard users navigate through interactive elements (like inputs, buttons, and links) in the order they appear in the DOM, not the order they appear visually. If the DOM order doesn’t match the intended workflow, users may tab through elements in a confusing sequence.

For example, in a form, the fields should appear in the JSX in the same sequence users are expected to fill them: first name, last name, email, and so on. Even if you use CSS grid or flexbox to rearrange the layout visually, keeping the DOM order logical ensures that the tabbing sequence remains intuitive.

To test this, try navigating your UI using only the Tab key. If the focus jumps unpredictably or skips elements, your DOM order likely needs adjustment. Following this simple practice prevents one of the most common keyboard accessibility problems.

Manage Focus

Proper focus management is important for components that open, close, or change dynamically, such as modals, popovers, dropdowns, or accordions. Most modern UI libraries, like MUI or Ant Design, already handle focus for these components out of the box, however when building custom components or extending library components, it’s important to make sure focus is managed correctly to prevent keyboard users from losing their place on the page.

Here are some practical focus management strategies for dynamic components, along with examples in React:

1. Move focus to the first interactive element inside the component when it opens:

For example, when a modal or popover opens, use a ref to focus the first element automatically:

	
const firstInputRef = useRef(null);

useEffect(() => {
  firstInputRef.current?.focus();
}, []);
	

2. Return focus to the element that triggered the component when it closes:

For example, after closing a dropdown, return focus to the button that opened it:

	
const triggerRef = useRef(null);

const handleClose = () => {
  triggerRef.current?.focus();
};
	

3. Trap focus inside the component if it’s modal-like, preventing users from tabbing to elements outside of it:

You can implement this manually by detecting Tab and Shift+Tab key presses and cycling focus between the first and last interactive elements inside the component:

	
const firstRef = useRef(null);
const lastRef = useRef(null);
	
const handleKeyDown = (e) => {
  if (e.key === 'Tab') {
    if (e.shiftKey && document.activeElement === firstRef.current) {
      e.preventDefault();
      lastRef.current.focus();
    } else if (!e.shiftKey && document.activeElement === lastRef.current) {
      e.preventDefault();
      firstRef.current.focus();
    }
  }
};
	

4. Allow users to close modals or popovers with Escape, and restore focus appropriately:

Listen for the Escape key and trigger the close function, then return focus to the trigger element:

	
useEffect(() => {
  const handleKey = (e) => {
    if (e.key === 'Escape') handleClose();
  };
            
  document.addEventListener('keydown', handleKey);
            
  return () => document.removeEventListener('keydown', handleKey);
}, []);
    

Use ARIA Attributes

ARIA attributes identify element roles, states, and properties to assistive technologies. Here are some of the most important attributes to include in your React components:

  • role – Defines the type of element for assistive technologies
    • Examples:
      • role="dialog" → modal dialogs
      • role="button" → custom clickable elements
      • role="tablist", role="tab", role="tabpanel" → tabs
      • role="menu", role="menuitem" → dropdown menus
  • aria-label – Provides an accessible label for elements without visible text
    • Examples:
      • Icon buttons: <button aria-label="Close modal" />
      • Custom input fields: <input aria-label="Search" />
  • aria-labelledby – References another element to serve as a label
    • Examples:
      • <div role="dialog" aria-labelledby="modal-title">
      • Tabs: <div role="tabpanel" aria-labelledby="tab-1">
  • aria-describedby – References content that describes the element
    • Examples:
      • Form fields: <input aria-describedby="email-help" />
      • Alerts: <div role="alert" aria-describedby="alert-text">
  • aria-expanded – Indicates if an element is expanded or collapsed
    • Examples:
      • Accordion headers: <button aria-expanded="true">Section 1</button>
      • Dropdown buttons: <button aria-expanded="false">Menu</button>
  • aria-hidden – Hides content from screen readers
    • Examples:
      • Decorative icons: <span aria-hidden="true">★</span>
      • Background content when a modal is open
  • aria-modal – Marks a modal-like element as modal
    • Examples:
      • <div role="dialog" aria-modal="true">
  • aria-checked – Indicates the state of checkboxes, switches, or menu items
    • Examples:
      • <input type="checkbox" aria-checked="true" />
  • aria-current – Marks the current item within a set
    • Examples:
      • Navigation links: <a aria-current="page">Home</a>
      • Step indicators in multi-step modals: <li aria-current="step">Step 1</li>
  • aria-live – Announces dynamic content changes to screen readers
    • Examples:
      • Notifications: <div aria-live="polite">New message received</div>
      • Form validation messages

Correctly applying these attributes ensures screen readers can interpret your interface, giving users the information they need to navigate effectively.

Checklist

  • Logical tab order ✅
  • Focus is always visible ✅
  • Escape closes modals, dropdowns etc. ✅
  • Arrow keys work complex elements ✅
  • ARIA attributes used correctly ✅

Conclusion

Keyboard accessibility doesn’t have to be a chore. With a few thoughtful patterns, focus management strategies, and proper use of ARIA attributes, you can build React components that feel intuitive to everyone, regardless of how they navigate your app.

Start small: check your tab order, make sure focus doesn’t get lost, and test your modals and menus with just the keyboard. Over time, these small habits add up to a more inclusive, professional app that’s easier for everyone to use.

If you want, you can take it a step further: combine these keyboard-friendly patterns with accessible forms, modals, and validation messages to create an interface that works smoothly for everyone. If you found this interesting, check out my guide on accessible forms in React for practical examples and tips you can use in your own projects.

Summary

An overview of the foundations of accessible UI in React: keeping a logical DOM order so keyboard users move through components predictably, managing focus so it never gets lost when elements appear or disappear, and using the right ARIA attributes to communicate purpose and state. It highlights examples like dropdowns, modals, and interactive controls where focus often breaks, and shows how proper structure and keyboard navigation makes every part of the interface usable without a mouse.

You can find the code for the examples in this post on my Github: https://github.com/mitevskasara/keyboard-navigation-react

Jump to Post Summary 🐇
Post a Comment (0)
Previous Post Next Post