Navigation API
TheNavigation API provides the ability to initiate, intercept, and manage browser navigation actions. It can also examine an application's history entries. This is a successor to previous web platform features such as theHistory API andwindow.location, which solves their shortcomings and is specifically aimed at the needs ofsingle-page applications (SPAs).
In this article
Concepts and usage
In SPAs, the page template tends to stay the same during usage, and the content is dynamically rewritten as the user visits different pages or features. As a result, only one distinct page is loaded in the browser, which breaks the expected user experience of navigating back and forth between different locations in the viewing history. This problem can be solved to a degree via theHistory API, but it is not designed for the needs of SPAs. The Navigation API aims to bridge that gap.
The API is accessed via theWindow.navigation property, which returns a reference to a globalNavigation object. Eachwindow object has its own correspondingnavigation instance.
Handling navigations
Thenavigation interface has several associated events, the most notable being thenavigate event. This is fired whenany type of navigation is initiated, meaning that you can control all page navigations from one central place, ideal for routing functionality in SPA frameworks. (This is not the case with theHistory API, where it is sometimes hard to detect and respond to all navigations.) Thenavigate event handler is passed aNavigateEvent object, which contains detailed information including details around the navigation's destination, type, whether it containsPOST form data or a download request, and more.
TheNavigationEvent object also provides two methods:
intercept()allows you to specify custom behavior for navigations, and can take the following as arguments:- Callback handler functions allowing you to specify what happens bothwhen the navigation is committed andjust before the navigation is committed. For example, you could load relevant new content into the UI based on the path of the URL navigated to, or redirect the browser to a sign-in page if the URL points to a restricted page and the user is not signed in.
- Properties that allow you to enable or disable the browser's default focus and scrolling behavior after the navigation occurs.
scroll()allows you to manually initiate the browser's scroll behavior (e.g., to a fragment identifier in the URL), if it makes sense for your code, rather than waiting for the browser to handle it automatically.
Once a navigation is initiated, and yourintercept() handler is called, aNavigationTransition object instance is created (accessible viaNavigation.transition), which can be used to track the process of the ongoing navigation.
Note:In this context "transition" refers to the transition between one history entry and another. It isn't related to CSS transitions.
Note:You can also callpreventDefault() to stop the navigation entirely for mostnavigation types; cancellation of traverse navigations is not yet implemented.
When the promises returned by theintercept() handler functions fulfill, theNavigation object'snavigatesuccess event fires, allowing you to run cleanup code after a successful navigation has completed. If they reject, meaning the navigation has failed,navigateerror fires instead, allowing you to gracefully handle failure case. There is also afinished property on the return value of navigation methods (such asNavigation.navigate()), which fulfills or rejects at the same time as the aforementioned events are fired, providing another path for handling the success and failure cases.
Note:Before the Navigation API was available, to do something similar you'd have to listen for all click events on links, rune.preventDefault(), perform the appropriateHistory.pushState() call, then set up the page view based on the new URL. And this wouldn't handle all navigations — only user-initiated link clicks.
Programmatically updating and traversing the navigation history
As the user navigates through your application, each new location navigated to results in the creation of a navigation history entry. Each history entry is represented by a distinctNavigationHistoryEntry object instance. These contain several properties such as the entry's key, URL, and state information. You can get the entry that the user is currently on right now usingNavigation.currentEntry, and an array of all existing history entries usingNavigation.entries(). EachNavigationHistoryEntry object has adispose event, which fires when the entry is no longer part of the browser history. For example, if the user navigates back three times, then navigates forward to somewhere else, those three history entries will be disposed of.
Note:The Navigation API only exposes history entries created in the current browsing context that have the same origin as the current page (e.g., not navigations inside embedded<iframe>s, or cross-origin navigations), providing an accurate list of all previous history entries just for your app. This makes traversing the history a much less fragile proposition than with the olderHistory API.
TheNavigation object contains all the methods you'll need to update and traverse through the navigation history:
navigate()Navigates to a new URL, creating a new navigation history entry.
reload()Reloads the current navigation history entry.
back()Navigates to the previous navigation history entry, if that is possible.
forward()Navigates to the next navigation history entry, if that is possible.
traverseTo()Navigates to a specific navigation history entry identified by its key value, which is obtained via the relevant entry's
NavigationHistoryEntry.keyproperty.
Each one of the above methods returns an object containing two promises —{ committed, finished }. This allows the invoking function to wait on taking further action until:
committedfulfills, meaning that the visible URL has changed and a newNavigationHistoryEntryhas been created.finishedfulfills, meaning that all promises returned by yourintercept()handler are fulfilled. This is equivalent to theNavigationTransition.finishedpromise fulfilling, when thenavigatesuccessevent fires, as mentioned earlier.- either one of the above promises rejects, meaning that the navigation has failed for some reason.
State
The Navigation API allows you to store state on each history entry. This is developer-defined information — it can be whatever you like. For example, you might want to store avisitCount property that records the number of times a view has been visited, or an object containing multiple properties related to UI state, so that state can be restored when a user returns to that view.
To get aNavigationHistoryEntry's state, you call itsgetState() method. It is initiallyundefined, but when state information is set on the entry, it will return the previously-set state information.
Setting state is a bit more nuanced. You can't retrieve the state value and then update it directly — the copy stored on the entry will not change. Instead, you update it while performing anavigate() orreload() — each one of these optionally takes an options object parameter, which includes astate property containing the new state to set on the history entry. When these navigations commit, the state change will be automatically applied.
In some cases however, a state change will be independent from a navigation or reload — for example when a page contains an expandable/collapsible<details> element. In this case, you might want to store the expanded/collapsed state in your history entry, so you can restore it when the user returns to the page or restarts their browser. Cases like this are handled usingNavigation.updateCurrentEntry(). Thecurrententrychange will fire when the current entry change is complete.
Limitations
There are a few perceived limitations with the Navigation API:
- The current specification doesn't trigger a
navigateevent on a page's first load. This might be fine for sites that use Server Side Rendering (SSR)—your server could return the correct initial state, which is the fastest way to get content to your users. But sites that leverage client-side code to create their pages may need an additional function to initialize the page. - The Navigation API operates only within a single frame—the top-level page, or a single specific
<iframe>. This has some interesting implications that arefurther documented in the spec, but in practice, will reduce developer confusion. The previousHistory API has several confusing edge cases, like support for frames, which the Navigation API handles up-front. - You can't currently use the Navigation API to programmatically modify or rearrange the history list. It might be useful to have a temporary state, for example navigating the user to a temporary modal that asks them for some information, then going back to the previous URL. In this case, you'd want to delete the temporary modal navigation entry so the user cannot mess up the application flow by hitting the forward button and opening it again.
Interfaces
NavigateEventExperimentalEvent object for the
navigateevent, which fires whenany type of navigation is initiated. It provides access to information about that navigation, and most notably theintercept(), which allows you to control what happens when the navigation is initiated.NavigationExperimentalAllows control over all navigation actions for the current
windowin one central place, including initiating navigations programmatically, examining navigation history entries, and managing navigations as they happen.NavigationActivationExperimentalRepresents a recent cross-document navigation. It contains the navigation type and current and destination document history entries.
NavigationCurrentEntryChangeEventExperimentalEvent object for the
currententrychangeevent, which fires when theNavigation.currentEntryhas changed. It provides access to the navigation type, and the previous history entry that was navigated from.NavigationDestinationExperimentalRepresents the destination being navigated to in the current navigation.
NavigationHistoryEntryExperimentalRepresents a single navigation history entry.
NavigationPrecommitControllerExperimentalDefines redirect behavior for a navigation precommit handler, when passed into the
precommitHandlercallback of aNavigateEvent.intercept()method call.NavigationTransitionExperimentalRepresents an ongoing navigation.
Extensions to other interfaces
Window.navigationRead onlyExperimentalReturns the current
window's associatedNavigationobject. This is the entry point for the Navigation API.
Examples
Note:Check out theNavigation API live demo (view demo source).
Handling a navigation usingintercept()
navigation.addEventListener("navigate", (event) => { // We can't intercept some navigations, e.g. cross-origin navigations. // Return early and let the browser handle them normally. if (!event.canIntercept) { return; } // We shouldn't intercept fragment navigations or downloads. if (event.hashChange || event.downloadRequest !== null) { return; } const url = new URL(event.destination.url); if (url.pathname.startsWith("/articles/")) { event.intercept({ async handler() { // The URL has already changed, so show a placeholder while // fetching the new content, such as a spinner or loading page renderArticlePagePlaceholder(); // Fetch the new content and display when ready const articleContent = await getArticleContent(url.pathname); renderArticlePage(articleContent); }, }); }});Handling scrolling usingscroll()
In this example of intercepting a navigation, thehandler() function starts by fetching and rendering some article content, but then fetches and renders some secondary content afterwards. It makes sense to scroll the page to the main article content as soon as it is available so the user can interact with it, rather than waiting until the secondary content is also rendered. To achieve this, we have added ascroll() call between the two.
navigation.addEventListener("navigate", (event) => { // Return early if we can't/shouldn't intercept if ( !event.canIntercept || event.hashChange || event.downloadRequest !== null ) { return; } const url = new URL(event.destination.url); if (url.pathname.startsWith("/articles/")) { event.intercept({ async handler() { const articleContent = await getArticleContent(url.pathname); renderArticlePage(articleContent); event.scroll(); const secondaryContent = await getSecondaryContent(url.pathname); addSecondaryContent(secondaryContent); }, }); }});Traversing to a specific history entry
// On JS startup, get the key of the first loaded page// so the user can always go back there.const { key } = navigation.currentEntry;backToHomeButton.onclick = () => navigation.traverseTo(key);// Navigate away, but the button will always work.await navigation.navigate("/another_url").finished;Updating state
navigation.navigate(url, { state: newState });Or
navigation.reload({ state: newState });Or if the state is independent from a navigation or reload:
navigation.updateCurrentEntry({ state: newState });Specifications
| Specification |
|---|
| HTML> # navigation-api> |