Which navigations does the `navigate` event on `window.navigation` fire for, and what does calling `event.intercept({ handler })` inside that listener do?
answer
- one event, every navigation
- not only back and forward
- take it over, don't cancel it
- the handler promise is the transition
- navigatesuccess or navigateerror
basics
~20 sThe 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 linesnavigation.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
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.
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.
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.
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