Skip to content

Navigation API

Published Updated

In one line: per MDN, “the Navigation API provides the ability to initiate, intercept, and manage browser navigation actions,” as a successor to the History API and window.location aimed at the needs of single-page applications (SPAs).

The API is accessed via Window.navigation, which per MDN “returns a reference to a global Navigation object” — each window has its own instance. Its most notable event is navigate, which per MDN’s Navigation API overview “is fired when any type of navigation is initiated, meaning that you can control all page navigations from one central place, ideal for routing functionality in SPA frameworks.” MDN’s navigate_event reference describes the same event more narrowly: it “fires when any type of navigation is initiated, allowing you to intercept as required.”

Per MDN’s browser-compat-data, the Navigation interface is supported in Chrome from version 102 (Chrome for Android from 102), in Firefox from version 147, and in Safari from version 26.2 (Safari on iOS from 26.2) — the compat data marks Edge, Opera, and Samsung Internet entries as "mirror", meaning their support data is derived from the corresponding upstream Chromium release rather than stated directly; it is not a claim that those browsers share Chrome’s own version numbers. The NavigateEvent interface itself shipped alongside it, also from Chrome 102; only its current intercept() method arrived slightly later, at Chrome 105 (an earlier, differently-named method, transitionWhile, existed from Chrome 102 until it was removed at Chrome 108). Firefox 147 and Safari 26.2 shipped NavigateEvent and intercept() together.

Listen for the navigate event and call intercept() to handle the navigation with custom logic instead of a full page load. This example assumes the page’s app shell already renders a <div id="article"></div> container to update:

navigation.addEventListener("navigate", (event) => {
// Some navigations, e.g. cross-origin navigations, cannot be intercepted —
// let the browser handle those normally.
if (!event.canIntercept) {
return;
}
// Don't intercept fragment navigations or downloads.
if (event.hashChange || event.downloadRequest !== null) {
return;
}
const url = new URL(event.destination.url);
event.intercept({
async handler() {
renderArticlePagePlaceholder();
const articleContent = await getArticleContent(url.pathname);
renderArticlePage(articleContent);
},
});
});
function renderArticlePagePlaceholder() {
document.querySelector("#article").innerHTML = "<p>Loading…</p>";
}
async function getArticleContent(pathname) {
const response = await fetch(`/api/articles${pathname}`);
return response.text();
}
function renderArticlePage(html) {
document.querySelector("#article").innerHTML = html;
}

Per MDN’s navigate_event reference, the URL has already updated by the time handler() runs, so its own example renders a placeholder immediately and swaps in real content once it is fetched — the pattern used above.

Feature-detect both navigation on window and NavigateEvent.prototype.intercept before registering handlers — per the compat data above, window.navigation shipped in Chrome 102 but intercept() did not arrive until Chrome 105, so checking window alone lets Chrome 102–104 through and then throws a TypeError on the first navigation. Fall back to real anchor-tag/History API navigation the browser already performs. This example assumes markup with a <div id="article"></div> container and in-page links (e.g. <a href="/articles/1">) already exist on the page:

function supportsNavigationApi() {
return (
"navigation" in window &&
typeof NavigateEvent !== "undefined" &&
typeof NavigateEvent.prototype.intercept === "function"
);
}
if (supportsNavigationApi()) {
navigation.addEventListener("navigate", handleAppNavigation);
} else {
// No usable Navigation API here — rely on regular link clicks and
// History API pushState/popstate for routing instead.
setupHistoryApiRouting();
}
function handleAppNavigation(event) {
if (
!event.canIntercept ||
event.hashChange ||
event.downloadRequest !== null ||
typeof event.intercept !== "function"
) {
return;
}
const url = new URL(event.destination.url);
event.intercept({
async handler() {
const response = await fetch(`/api/articles${url.pathname}`);
document.querySelector("#article").innerHTML = await response.text();
},
});
}
function setupHistoryApiRouting() {
// Intercept same-origin link clicks so they push a new History entry
// instead of triggering a full page load.
document.addEventListener("click", (event) => {
const link = event.target.closest("a[href]");
if (
!link ||
link.origin !== location.origin ||
event.defaultPrevented ||
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
) {
return;
}
event.preventDefault();
history.pushState(null, "", link.href);
renderArticleFromPath(location.pathname);
});
// Render the article matching the address bar on back/forward navigation.
window.addEventListener("popstate", () => {
renderArticleFromPath(location.pathname);
});
renderArticleFromPath(location.pathname);
}
async function renderArticleFromPath(pathname) {
const response = await fetch(`/api/articles${pathname}`);
document.querySelector("#article").innerHTML = await response.text();
}
  • Per MDN’s navigate_event reference, exit early (“Exit early if this navigation shouldn’t be intercepted”) for cases like event.hashChange or event.downloadRequest — not every navigate event should be intercepted.
  • Chrome’s intercept() method replaced an earlier, removed method named transitionWhile — code written against Chrome 102–107 samples using transitionWhile will not run on current browsers.
  • Per MDN’s compat data, NavigateEvent/intercept() landed in Firefox and Safari only at version 147 and 26.2 respectively, well after Chrome — sites still need the History API fallback path for any browser below those versions.
  • The navigate event is central to all navigation types per MDN, but not all of them can be intercepted: per MDN’s NavigateEvent.canIntercept reference, cross-origin navigations are one case where canIntercept is false, so a handler must check it and let the browser handle those navigations normally instead of trying to intercept them.