Focus Management is controlling which element receives focus during navigation, opening/closing modals, errors, and dynamic content changes.
System design context: Focus management is the backbone of keyboard accessibility. Without it, keyboard users get "lost" on the page — they cannot navigate to new content, they do not know where they are after a state change, and they cannot interact with dynamic elements like modals or toast notifications.
Example 1: Skip to content link:
1function SkipLink() {2 return (3 <a4 href="#main-content"5 className="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:bg-white focus:p-2 focus:rounded focus:shadow-lg"6 >7 Skip to main content8 </a>9 );10}1112function Layout() {13 return (14 <>15 <SkipLink />16 <Header />17 <main id="main-content" tabIndex={-1}>18 {/* Content */}19 </main>20 </>21 );22}
Example 2: Auto-focus after adding a todo:
1function TodoForm() {2 const [todos, setTodos] = useState<{ id: number; text: string; done: boolean }[]>([]);3 const inputRef = useRef<HTMLInputElement>(null);45 const addTodo = (text: string) => {6 setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);7 inputRef.current?.focus(); // Return focus to input8 };910 return (11 <form onSubmit={e => {12 e.preventDefault();13 const form = e.target as HTMLFormElement;14 const input = form.elements.namedItem("text") as HTMLInputElement;15 addTodo(input.value);16 form.reset();17 }}>18 <input ref={inputRef} name="text" aria-label="New task" />19 <button type="submit">Add</button>20 <ul>21 {todos.map(todo => (22 <li key={todo.id}>{todo.text}</li>23 ))}24 </ul>25 </form>26 );27}
Example 3: Focus on error:
1function LoginForm() {2 const [error, setError] = useState("");3 const errorRef = useRef<HTMLDivElement>(null);45 const handleSubmit = (e: React.FormEvent) => {6 e.preventDefault();7 const form = e.target as HTMLFormElement;8 const email = (form.elements.namedItem("email") as HTMLInputElement).value;910 if (!email.includes("@")) {11 setError("Please enter a valid email address.");12 // Move focus to the error message13 setTimeout(() => errorRef.current?.focus(), 0);14 return;15 }16 // Submit...17 };1819 return (20 <form onSubmit={handleSubmit}>21 {error && (22 <div23 ref={errorRef}24 role="alert"25 tabIndex={-1}26 className="bg-red-100 text-red-800 p-2 rounded"27 >28 {error}29 </div>30 )}31 <input name="email" type="email" aria-label="Email" />32 <button type="submit">Login</button>33 </form>34 );35}
Example 4: Route change focus management:
1import { useEffect } from "react";2import { useLocation } from "react-router-dom";34function RouteFocusManager() {5 const location = useLocation();67 useEffect(() => {8 // On route change, focus the main content area9 const main = document.getElementById("main-content");10 main?.focus();11 }, [location.pathname]);1213 return null;14}
Principles:
role="alert" + tabIndex={-1}).Common pitfalls:
element.focus() without tabIndex={-1} — focusable elements must be focusable even if they are not in the tab order.setTimeout(() => element.focus(), 0) or a ref callback.