Skip to content

React Tutorial Styling React Applications

Accessible Focus States in React: focus-visible and Keyboard Use

Lesson 88 of 156 3 min read By Deepak Paswan Updated Oct 10, 2026

Runnable lesson · press Try it on the examples to edit and run them here.

Focus is the element that receives keyboard input right now. People who use a keyboard, a switch device or a screen magnifier rely on a visible focus indicator to know where they are. Styling it well is one of the most valuable accessibility improvements you can make.

Never remove the outline without a replacement

The most common mistake:

script.php
PHP
* { outline: none; }   /* Do not do this */

It makes the page unusable with a keyboard. If you do not like the browser default, replace it with something at least as clear.

Use :focus-visible

:focus-visible shows the ring when the browser thinks it is needed: for keyboard focus, but not usually for mouse clicks on buttons. Click the button and then press Tab in the example below to see the difference.

App.jsx
JSX
function App() {
  return (
    <div>
      <style>{`
        .ctl { font: inherit; padding: 8px 14px; border: 2px solid #1a5276; border-radius: 6px; background: #fff; color: #1a5276; cursor: pointer; margin-right: 8px; }
        .ctl:focus-visible { outline: 3px solid #ca4b00; outline-offset: 3px; }
        .field { font: inherit; padding: 8px; border: 2px solid #888; border-radius: 6px; }
        .field:focus-visible { outline: 3px solid #ca4b00; outline-offset: 2px; border-color: #ca4b00; }
      `}</style>
      <p>Tab through these controls</p>
      <button className="ctl">One</button>
      <button className="ctl">Two</button>
      <input className="field" aria-label="Your name" placeholder="Name" />
    </div>
  );
}

Guidelines for a good focus ring:

  • At least 2px thick, and with a contrast ratio of 3:1 against the background and against the unfocused element.
  • Use outline (and outline-offset), not only box-shadow or border-color. Outlines also show in Windows High Contrast mode.
  • Make it consistent across the whole site. Define it once, in global CSS.
style.css
CSS
:focus-visible {
  outline: 3px solid #ca4b00;
  outline-offset: 2px;
}

Use real interactive elements

A <div onClick> cannot be focused and does not respond to Enter or Space. Use <button>, <a href> and form controls. If you must make something custom focusable, you need tabIndex={0}, a role, and keyboard handlers, which a native element gives for free.

Move focus when the screen changes

React updates the page without a reload, so the browser does not know that “you are on a new page” or “a dialog opened”. You have to manage focus:

EventWhere focus should go
A dialog opensInto the dialog (the first field or the heading); trap it inside; return it to the opener on close
A route change in a single-page appTo the new page’s heading or main region
An item is deleted from a listTo a sensible neighbour, or the list heading
A form fails validationTo the first invalid field, see Validation Messages

Use a ref and call focus() in an effect or event handler:

App.jsx
JSX
function App() {
  const [open, setOpen] = React.useState(false);
  const [status, setStatus] = React.useState("Dialog closed");
  const titleRef = React.useRef(null);
  const openerRef = React.useRef(null);

  React.useEffect(() => {
    if (open) titleRef.current.focus();   // move focus into the dialog
  }, [open]);

  function close() {
    setOpen(false);
    setStatus("Dialog closed");
    openerRef.current.focus();            // give focus back to the button that opened it
  }

  return (
    <div>
      <style>{`
        .dlg { border: 2px solid #333; padding: 12px; margin-top: 8px; background: #fff; max-width: 280px; }
        :focus-visible { outline: 3px solid #ca4b00; outline-offset: 2px; }
      `}</style>
      <p>Focus management</p>
      <button ref={openerRef} onClick={() => { setOpen(true); setStatus("Dialog open"); }}>Open dialog</button>
      {open && (
        <div className="dlg" role="dialog" aria-labelledby="t">
          <h3 id="t" tabIndex={-1} ref={titleRef}>Dialog title</h3>
          <button onClick={close}>Close</button>
        </div>
      )}
      <p role="status">{status}</p>
    </div>
  );
}

tabIndex={-1} makes the heading focusable by code but keeps it out of the Tab order. For real dialogs, use the native <dialog> element with showModal(), which traps focus and closes on Escape for you (see Dialog and Modal).

Keyboard users should not have to Tab through your whole menu on every page. Add a “Skip to content” link as the first focusable element:

App.jsx
JSX
<a className="skip-link" href="#main">Skip to content</a>
...
<main id="main" tabIndex={-1}>

Show it when it receives focus (.skip-link:focus { position: static; }) and keep it off screen otherwise.

Test it yourself

  1. Put your mouse away and press Tab through the page. Is the focus always visible?
  2. Does the order make sense? Does Shift+Tab go back?
  3. Can you use every control with Enter or Space?
  4. Does focus land in a sensible place after something opens, closes or navigates?

See Accessibility in React and Focus Management for more.

Key points

  • Never remove outlines without replacing them; use :focus-visible with a clear, high-contrast ring.
  • Use native interactive elements, which are focusable and keyboard-ready.
  • Manage focus yourself when a dialog opens, a route changes or an error appears.