function EditableName({ name }: { name: string }) {
const [editing, setEditing] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (editing) inputRef.current?.focus();
}, [editing]);
if (editing) return <input ref={inputRef} defaultValue={name} />;
return <button onClick={() => setEditing(true)}>Edit name</button>;
}
In React, you move focus with a ref and call .focus() on the DOM element after React has rendered it.
The usual steps:
- Create a ref with
useRefand attach it to the element. - After the state change, call
ref.current?.focus()in auseEffectthat depends on that state. Or call it in the event handler, if the element already exists. - Only move focus after a user action. Don't steal focus when the page loads.
Common cases:
- Edit mode: focus the input when it appears, as in the example. When editing ends, focus the Edit button again.
- Dialogs: focus inside on open. On close, return focus to the button that opened it. Save it first, for example
document.activeElement. - Deleting an item: focus the next item or the list heading.
- Route changes: focus the new page's
<h1>, which hastabIndex={-1}.
Other tips:
autoFocusworks for an element that mounts because of a user action, like a dialog input. Avoid it on page load, because it skips content for screen reader users.- Use
useIdto make unique ids forhtmlFor,aria-describedbyandaria-labelledbyin reusable components.
- Focus is moved with a ref and
.focus()after React renders the element. - The focus call goes in an effect that depends on the state, or in the event handler.
- Focus should not be moved on the first page load.
- Dialogs return focus to their opener, and deleted items pass focus to a sensible neighbour.
useIdcreates unique ids for label and ARIA references.