Skip to content
sales us5 min readApril 13, 2026

How I Stop My Single-Page App From Hijacking Keyboard Focus

A small business owner once told me her contact form on a fancy React site was invisible to her best customer, a blind attorney using NVDA. The form worked. The screen reader just never knew it existed. That is a focus management problem, and on sing

NP

Nikola Pantelin

Pantelin Creative Design

How I Stop My Single-Page App From Hijacking Keyboard Focus

A small business owner once told me her contact form on a fancy React site was invisible to her best customer, a blind attorney using NVDA. The form worked. The screen reader just never knew it existed. That is a focus management problem, and on single-page apps it is the silent reason you lose accessible users and ADA-compliant contracts.

Focus management in a SPA means deciding, on every route change and every modal open, exactly which DOM element receives keyboard focus next. Browsers do not do this for you when you swap views with JavaScript, so you have to.

Why SPAs break focus by default

When a regular website loads a new page, the browser resets focus to the document and screen readers announce the new title. In a React, Vue, or SvelteKit app, the URL changes but the document never reloads. Focus stays parked on the link the user clicked, which is now hidden, removed, or repositioned. Screen reader users hear nothing. Keyboard users tab into stale content.

I see three failure patterns over and over on client audits. Modal dialogs that open but leave focus on the trigger button behind them. Route transitions that leave focus on a now-unmounted nav item. Inline content swaps, like a "load more" button, that dump the user back at the top of the page.

The route change pattern I use

On every route change I move focus to a skip target at the top of the new view. In React Router I wrap the layout in a small component:

``jsx
function FocusOnRouteChange() {
const location = useLocation();
const ref = useRef(null);
useEffect(() => {
ref.current?.focus();
}, [location.pathname]);
return <h1 ref={ref} tabIndex={-1} className="sr-only-focusable">
{document.title}
</h1>;
}
``

The h1 carries the new page title, has tabIndex of negative one so it can receive programmatic focus without being in the tab order, and is visually hidden until focused. Screen readers announce the title. Sighted keyboard users see a thin outline at the top and tab onward into real content.

Modals: trap, restore, and label

A modal dialog has three jobs around focus. Move focus into the dialog when it opens. Trap Tab and Shift+Tab inside the dialog. Return focus to the element that opened it when it closes.

I use the native HTML dialog element where I can. It handles trapping for free in modern browsers. For older support I reach for a small library like focus-trap-react instead of writing it myself, because every hand-rolled focus trap I have ever shipped has had a bug with shadow DOM, iframes, or contenteditable. Pair it with aria-labelledby pointing at the modal heading and aria-describedby pointing at the first paragraph.

Inline updates without losing the user

When a user clicks "Load more comments" the new comments appear below. If I do nothing, focus stays on the button, which is fine. If the button itself disappears, I move focus to the first new comment with tabIndex negative one. The rule I follow: never let focus end up on document.body. That is the SPA equivalent of a 404 for keyboard users.

For toast notifications I do the opposite. I never steal focus. I render them inside a polite live region with role status so screen readers announce them without interrupting the user.

Testing this without buying enterprise tools

I run three checks on every SPA project before I call it done. First, I unplug my mouse and try to complete the primary conversion path. If I get stuck, that is a bug. Second, I open the page with VoiceOver on macOS or NVDA on Windows and navigate by heading and by landmark. If a section is unannounced, that is a bug. Third, I run axe DevTools and Lighthouse, but I treat them as smoke tests, not pass criteria. They miss most focus issues.

Quick comparison

PatternWhat to focusWhy
Route changeHidden h1 with new page titleScreen reader announces, keyboard users start at top
Modal openFirst focusable element insideTrap and restore on close
Drawer or sidebarThe drawer container itselfEscape returns focus to trigger
Inline load moreFirst new item if button removedNever strand focus on body
Form errorFirst invalid fieldLets user correct without hunting
## FAQ

Do I really need this if my site is a marketing page?
Yes if it has a single modal, a mobile menu, or any client-side route changes. That covers almost every modern marketing site I build.

Will Google care about focus management?
Google does not directly grade it, but Lighthouse accessibility scores feed into your overall page experience signals, and a site that fails ADA review can cost you a contract overnight in the US.

Is the native dialog element safe to use in 2026?
Yes. Every browser I support for paying clients handles it correctly now. I only polyfill for projects pinned to ancient Safari or embedded webviews.

How do I handle focus inside a server-driven HTMX or Turbo app?
Same principle, different hook. Listen for the swap event and call focus on a target element after the new HTML lands.

What about animations?
Move focus first, then animate the entrance. If you animate first, the first half-second of the screen reader announcement gets lost.

Ready to talk?

I'm Nikola, a solo freelance fractional growth partner working with small business owners across the US and EU. I build the websites, run the ads, set up the automations, and own the results. No junior account managers, no handoffs, no agency overhead.

If anything in this post sounded like a problem you're trying to solve, book a free 15-minute discovery call. I'll look at your current setup, point out the highest-leverage fix, and tell you honestly whether I'm the right person to help. No pitch, no obligation.

sales-us

Found this useful?

Subscribe to get articles like this delivered to your inbox. No spam.

Get in Touch