patternjavascriptMajor
Focus management in SPAs — restore and move focus on route changes
Viewed 0 times
focus managementspa navigationroute change focusscreen reader spareact router focus
Problem
In a Single Page Application, navigating between routes does not move browser focus or announce the page change to screen readers. Keyboard and screen reader users remain focused on the navigation link that triggered the transition, with no indication that new content has loaded.
Solution
On each route change: move focus to either a skip-link target, the main heading, or an announced region. Use a ref or querySelector to imperatively focus the target.
// React Router v6 example
import { useLocation } from 'react-router-dom';
import { useEffect, useRef } from 'react';
function RouteAnnouncer() {
const location = useLocation();
const headingRef = useRef(null);
useEffect(() => {
// Small timeout allows DOM to update before focusing
const id = setTimeout(() => {
headingRef.current?.focus();
}, 50);
return () => clearTimeout(id);
}, [location.pathname]);
return <h1 tabIndex={-1} ref={headingRef}>{document.title}</h1>;
}
// React Router v6 example
import { useLocation } from 'react-router-dom';
import { useEffect, useRef } from 'react';
function RouteAnnouncer() {
const location = useLocation();
const headingRef = useRef(null);
useEffect(() => {
// Small timeout allows DOM to update before focusing
const id = setTimeout(() => {
headingRef.current?.focus();
}, 50);
return () => clearTimeout(id);
}, [location.pathname]);
return <h1 tabIndex={-1} ref={headingRef}>{document.title}</h1>;
}
Why
Without focus management, screen reader users hear no confirmation that navigation succeeded. They can be stuck in a stale DOM context or must manually navigate from the top of the page on every route change.
Gotchas
- tabIndex={-1} makes a non-interactive element programmatically focusable without adding it to the tab order
- Do not focus the <body> — screen readers announce very little useful information for it
- Avoid autofocusing the first form field on load; instead focus a descriptive heading or landmark
- Next.js has a built-in route announcer component; check framework docs before rolling your own
Code Snippets
Reusable hook for focusing a heading on route change
// Generic focus-on-navigate hook
function useFocusOnNavigate(ref) {
const location = useLocation();
useEffect(() => {
const el = ref.current;
if (el) {
el.setAttribute('tabindex', '-1');
el.focus({ preventScroll: false });
}
}, [location.pathname]);
}Context
Building client-side routed applications (React Router, Vue Router, SvelteKit, Next.js)
Revisions (0)
No revisions yet.