Navigation API
Experimental: This is an experimental technology
Check the Browser compatibility table carefully before using this in production.
The Navigation 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 the History API and window.location
, which solves their shortcomings and is specifically aimed at the needs of single-page applications (SPAs).
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 the History API, but it is not designed for the needs of SPAs. The Navigation API aims to bridge that gap.
The API is accessed via the Window.navigation
property, which returns a reference to a global Navigation
object. Each window
object has its own corresponding navigation
instance.
Handling navigations
The navigation
interface has several associated events, the most notable being the navigate
event. This 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. (This is not the case with the History API, where it is sometimes hard to figure out responding to all navigations.) The navigate
event handler is passed a NavigateEvent
object, which contains detailed information including details around the navigation's destination, type, whether it contains POST
form data or a download request, and more.
The NavigationEvent
object also provides two methods:
intercept()
takes as an argument a callback handler function returning a promise. It allows you to control what happens when the navigation is initiated. For example, in the case of an SPA, it can be used to load relevant new content into the UI based on the path of the URL navigated to.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 your intercept()
handler is called, a NavigationTransition
object instance is created (accessible via Navigation.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 call preventDefault()
to stop the navigation entirely for most navigation types; cancellation of traverse navigations is not yet implemented.
When the intercept()
handler function's promise fulfills, the Navigation
object's navigatesuccess
event fires, allowing you to run cleanup code after a successful navigation has completed. If it rejects, meaning the navigation has failed, navigateerror
fires instead, allowing you to gracefully handle the failure case. There is also a finished
property on the NavigationTransition
object, 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, run e.preventDefault()
, perform the appropriate History.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 distinct NavigationHistoryEntry
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 using Navigation.currentEntry
, and an array of all existing history entries using Navigation.entries()
. Each NavigationHistoryEntry
object has a dispose
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 older History API.
The Navigation
object contains all the methods you'll need to update and traverse through the navigation history:
-
Navigates to a new URL, creating a new navigation history entry.
reload()
Experimental-
Reloads the current navigation history entry.
back()
Experimental-
Navigates to the previous navigation history entry, if that is possible.
forward()
Experimental-
Navigates to the next navigation history entry, if that is possible.
traverseTo()
Experimental-
Navigates to a specific navigation history entry identified by its key value, which is obtained via the relevant entry's
NavigationHistoryEntry.key
property.
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:
committed
fulfills, meaning that the visible URL has changed and a newNavigationHistoryEntry
has been created.finished
fulfills, meaning that all promises returned by yourintercept()
handler are fulfilled. This is equivalent to theNavigationTransition.finished
promise fulfilling, when thenavigatesuccess
event 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 a visitCount
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 a NavigationHistoryEntry
's state, you call its getState()
method. It is initially undefined
, 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 a navigate()
or reload()
— each one of these optionally takes an options object parameter, which includes a state
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 using Navigation.updateCurrentEntry()
. The currententrychange
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
navigate
event 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 are further documented in the spec, but in practice, will reduce developer confusion. The previous History 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
-
Event object for the
navigate
event, which fires when any 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. -
Allows control over all navigation actions for the current
window
in one central place, including initiating navigations programmatically, examining navigation history entries, and managing navigations as they happen. -
Represents a recent cross-document navigation. It contains the navigation type and current and destination document history entries.
-
Event object for the
currententrychange
event, which fires when theNavigation.currentEntry
has changed. It provides access to the navigation type, and the previous history entry that was navigated from. -
Represents the destination being navigated to in the current navigation.
-
Represents a single navigation history entry.
-
Represents an ongoing navigation.
Extensions to other interfaces
-
Returns the current
window
's associatedNavigation
object. This is the entry point for the Navigation API.
Examples
Note: Check out Domenic Denicola's Navigation API live demo.
Handling a navigation using intercept()
navigation.addEventListener("navigate", (event) => {
// Exit early if this navigation shouldn't be intercepted,
// e.g. if the navigation is cross-origin, or a download request
if (shouldNotIntercept(event)) {
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 using scroll()
In this example of intercepting a navigation, the handler()
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 a scroll()
call between the two.
navigation.addEventListener("navigate", (event) => {
if (shouldNotIntercept(event)) {
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 Standard # navigation-api |
Browser compatibility
api.Navigation
BCD tables only load in the browser
api.NavigationDestination
BCD tables only load in the browser
api.NavigationHistoryEntry
BCD tables only load in the browser
api.NavigationTransition
BCD tables only load in the browser