skip to content

The Navigation API

You will learn the modern replacement for the History API — a single navigate event you can intercept, with a real entry list and transition signals. Interviewers ask this to see whether you track where the platform is heading, not just where it has been.

on this pageshow

questions

5

Which navigations does the `navigate` event on `window.navigation` fire for, and what does calling `event.intercept({ handler })` inside that listener do?

level: middleimportance: must knowfreq 45%

answer

  1. one event, every navigation
  2. not only back and forward
  3. take it over, don't cancel it
  4. the handler promise is the transition
  5. navigatesuccess or navigateerror

basics

~20 s

The navigate event on window.navigation fires for navigations the page is involved in — link clicks, form submissions, fragment changes, back/forward traversals, and navigation.navigate() calls. event.intercept({handler}) turns the navigation into a same-document one your handler completes.

solid answer

~50 s

`window.navigation` fires a single `navigate` event for essentially every navigation the page takes part in: a user clicking a link, submitting a form, a fragment change, a back or forward traversal, and programmatic `navigation.navigate()` or `navigation.reload()`. That is the headline improvement — a router no longer has to hijack click and submit events itself and separately react to back/forward after the fact. Inside the listener you check `event.canIntercept` and then call `event.intercept({ handler })`. That tells the browser not to do a cross-document load: the navigation becomes same-document, the URL and history entry commit right away, and the promise your `handler` returns represents the rest of the transition. While it is pending `navigation.transition` is non-null; when it settles the browser fires `navigatesuccess` or `navigateerror`. Focus reset and scroll restoration happen for you unless you opt out via `focusReset` or `scrollBehavior`.

code

javascript · 13 lines
javascript
navigation.addEventListener("navigate", (event) => {
  if (!event.canIntercept || event.downloadRequest !== null) return;
  const { pathname } = new URL(event.destination.url);
  event.intercept({
    scrollBehavior: "after-transition",
    async handler() {
      const res = await fetch(`/api/page?path=${encodeURIComponent(pathname)}`, {
        signal: event.signal,
      });
      document.querySelector("#app").textContent = await res.text();
    },
  });
});

go deeper

for a junior

Know that window.navigation exists and fires a navigate event for link clicks, form submissions and back/forward alike, and that event.intercept() is how a router takes a navigation over.

for a middle

Be ready to explain the interception mechanics: the guard on canIntercept, that commit happens before the handler resolves, and that the handler's promise drives navigatesuccess or navigateerror.

for a senior

Show production judgment about the committed-early URL — loading states, aborting in-flight work through event.signal, and one global error path at navigateerror instead of try/catch per route.

for a principal

Own the argument for centralising navigation on one interception point: fewer bespoke click handlers, consistent focus and scroll behaviour, and a seam the team can test, weighed against maintaining a fallback path.

## What the old model forced routers to do Before the Navigation API, a client-side router had no single hook for "the page is about to navigate". It had to assemble one: attach a capturing `click` listener on `document`, filter out modifier-key clicks, `target="_blank"`, `download` links and cross-origin hrefs, call `preventDefault()`, push the new URL, and render. Form submissions usually went unhandled. Back and forward were a *separate* code path that only learned about the change after it had already happened. Scroll position and focus management were entirely the app's problem, which is why so many SPAs land you mid-page with focus stuck on the old view. ## One event for every navigation `window.navigation` is a `Navigation` object — an `EventTarget` — and it fires `navigate` for navigations of this page, including: - a user clicking a same-origin link, - a form submission (the event carries `formData`), - a fragment-only change (`event.hashChange` is `true`), - a back or forward traversal (`event.navigationType === "traverse"`), - programmatic `navigation.navigate(url)`, `navigation.reload()`, `navigation.back()`, `navigation.forward()`, `navigation.traverseTo(key)`. It does **not** fire for navigations the user starts outside the page, such as typing a new URL in the address bar, and it does not report navigations of other windows or frames. Useful properties on the event: `navigationType` (`"push"`, `"replace"`, `"reload"`, `"traverse"`), `destination` (a `NavigationDestination` with `url`, `sameDocument`, and for traversals `key`, `id`, `index` and `getState()`), `userInitiated`, `canIntercept`, `downloadRequest`, `info` (arbitrary data you passed to `navigation.navigate(url, { info })`, useful for hints like animation direction), and `signal`, an `AbortSignal` that aborts if the navigation is superseded or cancelled. ## What intercept() actually changes ```js navigation.addEventListener("navigate", (event) => { if (!event.canIntercept) return; event.intercept({ async handler() { await render(new URL(event.destination.url).pathname, event.signal); }, }); }); ``` Calling `intercept()` converts what would have been a cross-document load into a same-document navigation. Three things follow: 1. **Commit is immediate by default.** The URL in the address bar changes, `navigation.currentEntry` becomes the new entry, and `currententrychange` fires — before your handler resolves. Newer Chromium versions add a `commit: "after-transition"` option plus `event.commit()` so you can delay that, but the default is immediate. Design your UI for "URL first, content shortly after", the same as a server navigation showing a loading indicator. 2. **The handler promise is the transition.** While it is pending, `navigation.transition` is a `NavigationTransition` carrying `navigationType`, the `from` entry, and a `finished` promise. When your handler resolves the browser fires `navigatesuccess`; when it rejects it fires `navigateerror` with the error. That gives you one place to render a global error state rather than a try/catch in every route. 3. **Focus and scrolling are handled.** `focusReset` defaults to `"after-transition"`, which moves focus to the document's autofocus target or the body — the accessibility behaviour most hand-rolled routers get wrong. `scrollBehavior` also defaults to `"after-transition"`; set it to `"manual"` and call `event.scroll()` yourself when you want to scroll before the data has finished loading. More than one listener may call `intercept()`; all the handlers run and the transition finishes when all of them settle. ## Intercept versus cancel `intercept()` says "this navigation proceeds, but I am rendering it". `preventDefault()` — permitted when `event.cancelable` is true — says "this navigation does not happen at all", which is how you build an unsaved-changes guard without `beforeunload`. Using `preventDefault()` and then changing the URL yourself is the anti-pattern the API exists to retire. ## Boundaries to remember The event fires for cross-origin destinations too, but `canIntercept` is `false` there and calling `intercept()` throws, so always guard. Downloads (`downloadRequest` non-null) are likewise not interceptable. And because `window.navigation` is absent in engines that have not shipped the API, every production use starts with a feature check and keeps a fallback path.

  • What is the difference between calling `event.preventDefault()` and `event.intercept()` in a navigate listener?
    `preventDefault()`, allowed when `event.cancelable` is true, stops the navigation outright — the URL never changes. That is how you build an unsaved-changes guard. `intercept()` lets the navigation happen but converts it to a same-document one that your handler renders. Cancelling and then setting the URL yourself defeats the whole point of the API.
  • What is `event.info` for, and how does it differ from history state?
    `info` is a one-shot value you pass to `navigation.navigate(url, { info })` or `navigation.back({ info })`, and read as `event.info` in the listener. It is not serialized and not stored in the entry, so it disappears on reload or traversal. Use it for transition hints — animation direction, "came from the search box" — never for state you need to restore.
  • Your intercept handler's promise rejects halfway through. What state is the page in?
    The URL and history entry already committed, so the address bar shows the new route while the DOM may be half-updated. `navigation.transition.finished` rejects and `navigateerror` fires with the error, which is your single hook for rendering an error view. Treat the handler like any async render: catch what you can recover from, and let the rest surface at `navigateerror`.

It is the difference between posting one doorman at the building's single entrance and tackling people individually at every door, window and fire escape.

saying these in an interview costs you the question

  • Says navigate only fires for back and forward, like popstate
  • Calls preventDefault() and then updates the URL manually to route
  • Thinks intercept() holds the URL until the handler resolves
  • Assumes address-bar navigations fire the navigate event
  • Skips canIntercept and lets cross-origin links throw

context

open as a page

How do you feature-detect the Navigation API in a browser, and what do you fall back to when it is unavailable?

level: juniorimportance: should knowfreq 28%

basics

~20 s

Test for the object itself with 'navigation' in window before using it. When it is missing, fall back to the History API path: pushState plus a popstate listener, with your own click and submit interception.

open as a page

Inside a `navigate` event listener on `window.navigation`, when is `event.canIntercept` false, and what should your code do in that case?

level: middleimportance: should knowfreq 32%

basics

~20 s

event.canIntercept is false when the browser cannot turn the navigation into a same-document one — a cross-origin destination, a download link, or a cross-document back/forward traversal. Return early and let the browser navigate; calling intercept() then throws.

open as a page

A single-page app wants to send the user back to one specific earlier view after a multi-step flow. How do `navigation.entries()`, `navigation.currentEntry` and `navigation.traverseTo()` make that possible, and why is it more reliable than stepping back a fixed number of entries?

level: seniorimportance: should knowfreq 24%

basics

~20 s

Save navigation.currentEntry.key before the flow starts, then call navigation.traverseTo(key) to return to exactly that entry. A fixed number of back steps is unreliable because redirects, replacements and the user's own navigation change how many entries lie in between.

open as a page

Your web app's client-side routing is built directly on the History API. How would you decide whether to move it onto the Navigation API, and how would you sequence that change given uneven browser support?

level: principalimportance: nice to knowfreq 16%

basics

~20 s

Decide from what the current routing code costs you — bespoke click interception, focus and scroll bugs, no real transition lifecycle — against maintaining two engines while support is uneven. Sequence it behind one router interface, enable the new path progressively, and delete the fallback on telemetry.

open as a page