Problem and Use Cases
A full document navigation replaces the document and its title. A soft SPA navigation keeps the same document, so focus can remain on a link whose surrounding content disappeared. A screen-reader user may hear no useful indication that a new product, checkout step, or error page is ready.
The policy must cover five distinct transitions: initial load, a user-initiated route change, an in-place filter or sort, a replaced or cancelled navigation, and browser Back/Forward. It must also coexist with component-owned behavior such as dialogs. The goal is predictable context, not moving focus after every URL mutation.
What the Interviewer Is Evaluating
The interviewer is looking for a semantic decision model. “Focus the heading on every route change” sounds accessible but breaks filters, hash changes, initial hydration, and history restoration. A strong answer first decides whether the user's task changed.
They also expect browser knowledge: a heading can receive programmatic focus with tabIndex={-1} without joining the sequential Tab order; focus() normally scrolls the element; preventScroll separates focus restoration from scroll restoration; and positive tabindex values create fragile focus order.
Finally, the answer must handle concurrency. If route B resolves after route C, B must not update the title or steal focus. Automated checks can prove the active element, title, and order, but cannot prove what every browser and assistive-technology pair announces.
Clarifying Questions Before Answering
- Which transitions start a new task? Product-to-checkout does; changing a sort order usually does not. This determines whether focus moves or stays on the initiating control.
- When is a route committed? Focus should run only when the winning route has rendered its final primary heading, not on click, URL mutation, or skeleton display.
- What owns overlays? A dialog keeps its own initial focus, trap, and return-focus contract. Route policy must not compete with it.
- What does history restore? Decide whether the product requires focus, scroll, or both per history entry. The two need coordination to avoid a double jump.
- Which browsers and assistive technologies are supported? Announcement behavior varies, so the release matrix must be explicit.
- Can every route provide a descriptive title and one primary heading? Make these part of the route contract; use the main landmark only as a controlled fallback.
30-Second Answer Framework
“I classify navigation before moving focus. Initial load does nothing. A committed user-initiated task change updates the document title and focuses the new descriptive H1, made programmatically focusable with tabIndex=-1. Same-task filters preserve the control and report completed results through a persistent polite status region. Each navigation has a token, so only the latest committed route may apply title and focus. On Back/Forward I restore a valid semantic target saved for that history entry, otherwise I use the H1, coordinating focus with scroll restoration. I test initial load, rapid navigation, errors, filters, history, keyboard order, visible focus, and real screen readers.”
Step-by-Step Deep Dive
Start with a transition table:
| Transition | Focus action | Announcement |
|---|---|---|
| Initial load or hydration | Do not force focus | Native document/title behavior |
| PUSH to a new task | Focus committed route H1 | Updated title plus focused heading |
| Same-task filter, sort, or pagination | Preserve initiating control | Polite result/status update if useful |
| POP from Back/Forward | Restore valid saved target; otherwise H1 | Restored context or focused heading |
| Redirect or error route | Focus that route's committed H1 | Its final title and heading |
| Dialog open/close | Defer to dialog contract | Dialog label and return target |
The route contract supplies a final title, a descriptive H1 ref, and a semantic route key. Give the H1 tabIndex={-1}. It remains outside the normal Tab sequence, but code can focus it. Keep a visible focus indicator. A skip link remains the first keyboard-focusable control and targets main; it solves repeated-navigation bypass and still has value when route focus is managed.
Focus belongs to the commit boundary. Assign every attempted navigation a monotonically increasing token. When data and UI for a route finish, apply effects only if its token is still current and the H1 is mounted. Do not use a fixed timeout: network speed and rendering work make it unreliable.
beginNavigation(kind):
token = nextToken()
rememberCurrentFocus(historyEntryKey)
commitRoute(token, kind, title, heading):
if token != currentToken or heading is not connected:
return
document.title = title
if kind is PUSH or semantic-task REPLACE:
heading.focus()
else if kind is POP:
focus(validSavedTarget(historyEntryKey) or heading)Save a stable route-local focus ID rather than a generated CSS path. On POP, restore it only if the element still exists, is visible, enabled, and meaningful in the restored state. Otherwise use the H1. If the router restores scroll independently, use focus({ preventScroll: true }) and then restore scroll once; for a normal PUSH, allowing focus to reveal the H1 is usually clearer.
Avoid duplicate speech. Focusing the new H1 after updating the title often supplies sufficient context, although exact speech varies by assistive technology. Do not also announce the same title in a live region by default. For a filter that keeps focus, a persistent role="status" or aria-live="polite" region can announce the completed result count. Reserve assertive announcements for genuinely urgent information because they may interrupt current speech.
The test oracle follows the policy. On initial hydration, the application must not steal focus. On PUSH, after the final commit, document.activeElement is the new H1, the title is final, and the next Tab reaches the first logical interactive element. On a filter change, the trigger retains focus and the status text updates once. In an A→B→C race where B resolves last, C keeps title and focus. Error and redirect routes focus their own heading. POP restores a valid saved target or deterministically falls back.
Run automated DOM checks for all of those cases, including target unmounts and reduced-motion/zoom layouts. Then manually traverse with keyboard and supported combinations such as VoiceOver/Safari and NVDA or JAWS with a supported browser. Verify visible focus, logical order, no unexpected scroll, and understandable announcements. Record browser and assistive-technology versions because speech output is an integration result, not a DOM guarantee.
High-Quality Sample Answer
“I would make focus behavior part of the router's semantic contract. Each route exposes a final document title and a descriptive H1 ref. I classify the transition: initial hydration does not move focus; a user-initiated PUSH that changes the task focuses the final H1 after commit; a filter or sort preserves the initiating control; and POP attempts to restore a stable saved target before falling back to the H1.
The H1 has tabIndex=-1, so it is programmatically focusable without adding an extra Tab stop. I preserve a visible focus style and keep a skip link targeting main. Title and focus change together only after the winning route is committed. Every navigation receives a token, and an older async completion is ignored, which prevents a cancelled route from stealing focus.
For history, I store a semantic route-local focus ID per history entry. I restore only a connected, visible, enabled target. If scroll restoration is separate, I focus with preventScroll and restore scroll once. Same-task asynchronous updates use a persistent polite status region; I avoid repeating the route title in both the heading and live region.
I would automate assertions for active element, title, Tab order, filter retention, rapid A→B→C navigation, redirects, errors, and POP fallback. I would then test actual speech, visible focus, and scrolling with the supported keyboard and screen-reader matrix. That separates deterministic DOM behavior from assistive-technology behavior we must observe.”
Common Mistakes
- Focus on every URL change → filters and hash changes interrupt the task → classify semantic transitions first.
- Focus when the link is clicked → the destination may not exist or may be cancelled → focus only the winning committed route.
- Use a timeout → slow and fast renders race differently → use route lifecycle and a navigation token.
- Focus
bodyor add positivetabindex→ context and order become unclear → use a descriptive H1 withtabIndex=-1. - Always jump to the H1 on POP → Back loses the user's place → restore a valid history target with a deterministic fallback.
- Announce the title twice → users hear redundant speech → choose focused-heading context first and use live status for in-place updates.
- Treat an automated accessibility scan as proof → it cannot validate actual spoken output → add manual keyboard and assistive-technology tests.
Follow-up Questions and Responses
Follow-up 1: Why not always focus the main landmark?
An H1 usually gives a more specific name for the new task. main is a reasonable fallback when the route contract cannot supply a heading, but making every route provide one improves visible structure, document outline, and testability.
Follow-up 2: Should mouse navigation also move focus?
If a user-initiated action replaces the main task, consistent context is useful regardless of input device, including for screen-reader users who click. Base the decision on semantic navigation, not a guess about keyboard use. Avoid moving focus for background refreshes.
Follow-up 3: What if B finishes after C in rapid navigation?
B's token no longer matches the current navigation, so its commit effect returns without changing title, focus, or status. Aborting B's request saves work, but the token check is still required because cancellation may be late or unsupported.
Follow-up 4: How do focus and scroll restoration interact?
Normal focus() may scroll the target into view. On POP, if the router separately restores a saved scroll position, use preventScroll, validate and focus the saved element, then perform one scroll restoration. Define one owner so focus and router code do not fight.
Follow-up 5: When should a live region be assertive?
Only when delayed hearing creates a serious problem, such as an urgent session or safety message. Ordinary result counts and completion updates should be polite. Route context normally comes from the final title and focused heading, avoiding a second announcement.
Follow-up 6: Is this WCAG compliance by itself?
No. It supports understandable focus order and context in a dynamic application, but compliance also depends on semantics, names, keyboard operation, contrast, error handling, and other criteria. Test the complete user journey against the product's conformance target.