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:
* { 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.
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(andoutline-offset), not onlybox-shadoworborder-color. Outlines also show in Windows High Contrast mode. - Make it consistent across the whole site. Define it once, in global 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:
| Event | Where focus should go |
|---|---|
| A dialog opens | Into 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 app | To the new page’s heading or main region |
| An item is deleted from a list | To a sensible neighbour, or the list heading |
| A form fails validation | To the first invalid field, see Validation Messages |
Use a ref and call focus() in an effect or event handler:
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).
Skip links
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:
<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
- Put your mouse away and press Tab through the page. Is the focus always visible?
- Does the order make sense? Does Shift+Tab go back?
- Can you use every control with Enter or Space?
- 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-visiblewith 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.