100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace

How Do You Manage Focus in a Single-Page Application?

Learn how to manage keyboard focus after route changes in an SPA so screen reader and keyboard users are never lost.

mediumQ161 of 224 in Web Development Est. time: 5 minsLast updated:
Open Code Lab

Expected Interview Answer

Focus management in an SPA means programmatically moving keyboard focus to a sensible element — usually the new view’s heading — after every client-side route change or dynamic content update, because unlike a traditional page load, the browser never resets focus for you.

In a traditional multi-page site, navigating to a new URL causes a full document load, and the browser automatically resets focus to the top of the document, which screen reader users rely on as an implicit signal that content changed. An SPA swaps content via JavaScript without a real navigation, so focus silently stays on the now-removed link or button, leaving screen reader and keyboard users stranded with no indication anything happened. The fix is to move focus explicitly: after a route change, set focus to the new page’s main heading or a skip-to-content target using a ref and tabIndex={-1} so the element is programmatically focusable without adding it to the normal tab order. The same principle applies to modals (trap and return focus), toasts, and any dynamically injected content region — focus must always land somewhere meaningful, and screen readers should be told a route change happened, often via an ARIA live region announcing the new page title.

  • Gives screen reader users an audible cue that navigation actually happened
  • Prevents keyboard focus from being lost on a removed DOM node
  • Matches the implicit behavior sighted mouse users get automatically on page load
  • Establishes a consistent pattern reusable for modals, drawers, and toasts

AI Mentor Explanation

Focus management in an SPA is like a stadium announcer who must actively redirect a blind fan’s attention when play shifts to a new innings, because unlike a full match restart with a new toss ceremony, nothing automatically signals the change. Without the announcer calling out 'new innings, here is the new batting side,' the fan keeps listening to a commentary feed that has quietly moved on without them. The announcer explicitly points attention to the new information, just as code must explicitly move keyboard focus to the new view’s heading. That deliberate redirection is what prevents the fan from being lost mid-transition.

Step-by-Step Explanation

  1. Step 1

    Detect the route change

    Hook into the router (e.g. useEffect on pathname) to know exactly when a new view has mounted.

  2. Step 2

    Target the new heading

    Give the page's main h1 a ref and tabIndex={-1} so it is programmatically focusable but not in normal tab order.

  3. Step 3

    Move focus imperatively

    Call headingRef.current.focus() once the new view renders, after the DOM has painted.

  4. Step 4

    Announce via a live region

    Update an aria-live="polite" region with the new page title so screen readers narrate the change.

What Interviewer Expects

  • Explanation of why SPAs break the browser's default focus-reset behavior
  • Concrete technique: ref + tabIndex={-1} + imperative .focus()
  • Mention of ARIA live regions for announcing route changes
  • Awareness that the same pattern applies to modals and dynamic content, not just routing

Common Mistakes

  • Assuming React Router or similar handles focus automatically (it does not)
  • Adding tabIndex={-1} without also calling .focus() imperatively
  • Moving focus to the document body instead of a meaningful heading
  • Forgetting to return focus to the trigger element after closing a modal

Best Answer (HR Friendly)

In a single-page app, clicking a link does not reload the page, so the browser never resets keyboard focus like it normally would. I handle this by moving focus to the new page’s heading in code right after the route changes, and I use a live region so screen reader users hear that the page actually changed.

Code Example

Focusing the new view heading after a route change
import { useEffect, useRef } from 'react'
import { usePathname } from 'next/navigation'

function PageHeading({ title }: { title: string }) {
  const headingRef = useRef<HTMLHeadingElement>(null)
  const pathname = usePathname()

  useEffect(() => {
    headingRef.current?.focus()
  }, [pathname])

  return (
    <h1 ref={headingRef} tabIndex={-1}>
      {title}
    </h1>
  )
}

Follow-up Questions

  • How do you trap and restore focus correctly for a modal dialog?
  • What is the difference between aria-live="polite" and “assertive”?
  • Why use tabIndex={-1} instead of tabIndex={0} on the heading?
  • How would you test that focus management works without a screen reader?

MCQ Practice

1. Why does focus management matter specifically in SPAs?

A full page load resets focus automatically; a client-side route swap does not, so it must be done manually.

2. What does tabIndex={-1} accomplish on a heading?

tabIndex={-1} allows .focus() to work via JavaScript while excluding the element from sequential keyboard tabbing.

3. What should accompany a moved focus target for screen reader users?

A live region gives an audible announcement, reinforcing that focus moved because content genuinely changed.

Flash Cards

Why do SPAs need manual focus management?Client-side routing skips the browser's automatic focus reset on page load.

Typical focus target after a route change?The new view's main heading, via ref and imperative .focus().

What attribute makes a heading focusable but not tabbable?tabIndex={-1}.

What complements moved focus for screen readers?An aria-live region announcing the new page.

1 / 4

Continue Learning