skip to content

After a client-side route change, why must the framework restore scroll and move focus itself?

level: middleimportance: should knowfreq 56%

answer

  1. the document never changed
  2. browser only helps on a real load
  3. scroll depends on the kind of move
  4. focus falls to body when its element unmounts
  5. nothing is announced unless you announce it

basics

~20 s

Scroll, focus and announcement are handled by the browser only on a real document load. A script-driven view swap leaves scroll where it was, focus on an element that may be gone, and says nothing to assistive technology.

solid answer

~50 s

When the browser loads a document it resets scroll for a new entry and restores it on back and forward, puts focus at the start of the new document, and assistive technology announces the new page. A client navigation does none of that for free: the document never changed, so the browser sees only a mutated tree. The framework must record the scroll offset of the entry being left and restore it on a back or forward move, scroll to the top for a genuinely new route, and decide when to leave scroll alone — a filter change that only rewrites the query should not throw the user back to the top. It must also move focus into the newly rendered region, so keyboard users continue from the right place rather than from a removed element, and announce the change so screen-reader users learn the view moved at all.

go deeper

for a junior

Remember that a client navigation does not reload the document, so scroll position, focus and any announcement are the framework's responsibility, not the browser's.

for a middle

Explain the three cases scroll has to distinguish — new route, history move, same-route query change — and why focus falls to the body when its element unmounts.

for a senior

Demonstrate the timing problem: restoring into streamed content that has not laid out yet, and the flash that follows committing the URL before the new segments are meaningful.

for a principal

Treat this as a product-wide contract rather than per-page cleverness: one navigation policy, one place that decides scroll, focus and announcement, and a test that catches regressions with the keyboard.

A client navigation changes what is on screen without loading a document. That is the whole point — and it is also why three behaviours a user depends on quietly disappear unless the framework rebuilds them: **scroll position**, **focus**, and **announcement**. ## What the browser gives you for free, and only on a document load For a real document load the browser: - starts a new entry at the top of the page, and restores the saved offset when the user goes back or forward; - resets focus to the start of the new document, so the next keystroke acts on the new page; - exposes a new document to the accessibility layer, which is what makes a screen reader announce the new page. After a script-driven view swap the document is the same document. Nothing above happens. From the browser's point of view some nodes were replaced; from the user's point of view the page changed completely. ## Scroll: three different expectations under one mechanism The correct behaviour depends on the kind of move, which is why a single rule always feels wrong somewhere: | Kind of move | What the user expects | |---|---| | Forward to a new route | start at the top | | Back or forward through history | land exactly where they left | | Same route, changed query (a filter, a sort, a tab) | stay put | | Navigation to an in-page anchor | scroll to that target | So the framework has to record the offset for the entry it is leaving, keep those offsets per history entry, and classify the move before deciding. Three failure modes are near-universal: - **The new-route rule applied to a same-route change** — a filter panel writes a history entry on every keystroke and bounces the user to the top of a long list each time. - **Restoring before the page has height** — with streamed or lazily loaded content the saved offset is clamped to the current scroll height, so the user lands near the top anyway; restoration has to wait for the content that determines height, or be re-applied when it arrives. - **Ignoring a fragment target** — a link to an anchor inside the same route should scroll to that element, not follow the new-route rule. ## Focus: the keyboard user's cursor Focus lives on an element. When a navigation unmounts that element, focus does not helpfully move to the new content — it falls back to the document body. The next Tab starts from the very beginning of the page, so a keyboard user who clicked the fifth item in a sidebar is dropped back at the browser chrome and has to tab through the whole shell again to reach what they just opened. The framework's job is to place focus deliberately in the newly rendered region after the move, and to leave it alone for a change that did not really move the user. Which element deserves focus, and the markup that makes an arbitrary container focusable without adding it to the tab order, is general accessibility craft; the routing-level obligation is simply that something must decide, on every navigation. ## Announcement: telling assistive technology the view changed A sighted user sees the new view immediately. A screen-reader user gets nothing, because no new document was loaded and a silent DOM mutation is not announced. The framework has to say it explicitly — typically by updating the document title for the new route and by putting a short message into a region the assistive layer watches, so the user hears that navigation happened and where they landed. Without it, the most common report is not that the app is inaccessible but that it is confusing: activating a link seems to do nothing at all. ## Timing is the hard part All three of these depend on when the new content exists. If a route streams in, if a segment suspends on data, or if an image decides the final height, then doing the work at the moment the URL changes is too early and doing it when everything settles is too late — the user has already seen a flash at the wrong offset. Practical implementations commit the URL, wait for the new segments to render enough to be meaningful, then apply scroll and focus together, and re-apply scroll once if late content shifts the layout. ## Where frameworks differ Most ship a default that handles the common cases: top on a new route, restore on back and forward, and some focus handling. They differ in how much they announce, whether a same-route query change preserves scroll by default, and what escape hatch they give you per navigation. The details vary; the obligation does not — if your router does not do these three things, no one else will.

  • Why does restoring a saved scroll offset sometimes leave the user near the top anyway?
    Because the offset is applied before the page is tall enough to accept it. With streamed or lazily loaded content, the restore is clamped to the current scroll height and the extra height arrives afterwards. The fix is to defer restoration until the segments that determine height have rendered, and re-apply it if late content changes the layout.
  • Why should a filter or sort change be treated differently from a route change?
    Because the user has not moved — they are refining what they are already looking at. Applying the new-route rule scrolls them to the top of a long list on every keystroke, which reads as the page resetting itself. Treat a same-route change to the query as an update in place: keep scroll, and usually keep focus where the user put it.
  • What is the user-visible symptom when a client router never announces navigations?
    A screen-reader user activates a link and hears nothing, so the app appears to have ignored them; they often activate it repeatedly. Nothing is broken visually, which is why it survives testing. Updating the title for the new route and announcing the landing point through a watched region is the minimum fix.

saying these in an interview costs you the question

  • Assumes the browser restores scroll after a script-driven view change.
  • Scrolls to the top on every URL change, including filter and sort updates.
  • Never moves focus, leaving keyboard users at the top of the shell.
  • Thinks a visible change is automatically announced by assistive technology.
  • Restores scroll immediately, before streamed content has given the page height.
  • Treats announcement as optional polish rather than part of navigating.