Navigation API

Draft Community Group Report,

This version:
https://wicg.github.io/navigation-api/
Editor:
Domenic Denicola (Google)
Participate:
GitHub WICG/navigation-api (new issue, open issues)
Commits:
GitHub spec.bs commits

Abstract

The navigation API provides a web application-focused way of managing same-origin same-frame history entries and navigations.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. The Navigation class

partial interface Window {
  [Replaceable] readonly attribute Navigation navigation;
};

Each Window object has an associated navigation API, which is a new Navigation instance created alongside the Window.

The navigation getter steps are to return this's navigation API.

[Exposed=Window]
interface Navigation : EventTarget {
  sequence<NavigationHistoryEntry> entries();
  readonly attribute NavigationHistoryEntry? currentEntry;
  undefined updateCurrentEntry(NavigationUpdateCurrentEntryOptions options);
  readonly attribute NavigationTransition? transition;

  readonly attribute boolean canGoBack;
  readonly attribute boolean canGoForward;

  NavigationResult navigate(USVString url, optional NavigationNavigateOptions options = {});
  NavigationResult reload(optional NavigationReloadOptions options = {});

  NavigationResult traverseTo(DOMString key, optional NavigationOptions options = {});
  NavigationResult back(optional NavigationOptions options = {});
  NavigationResult forward(optional NavigationOptions options = {});

  attribute EventHandler onnavigate;
  attribute EventHandler onnavigatesuccess;
  attribute EventHandler onnavigateerror;
  attribute EventHandler oncurrententrychange;
};

dictionary NavigationUpdateCurrentEntryOptions {
  required any state;
};

dictionary NavigationOptions {
  any info;
};

dictionary NavigationNavigateOptions : NavigationOptions {
  any state;
  NavigationHistoryBehavior history = "auto";
};

dictionary NavigationReloadOptions : NavigationOptions {
  any state;
};

dictionary NavigationResult {
  Promise<NavigationHistoryEntry> committed;
  Promise<NavigationHistoryEntry> finished;
};

enum NavigationHistoryBehavior {
  "auto",
  "push",
  "replace"
};

Each Navigation object has an associated entry list, a list of NavigationHistoryEntry objects, initially empty.

Each Navigation object has an associated current entry index, an integer, initially −1.

A Navigation navigation has entries and events disabled if the following steps return true:
  1. Let browsingContext be navigation’s relevant global object's browsing context.

  2. If browsingContext is null, then return true.

  3. If browsingContext is still on its initial about:blank Document, then return true.

  4. If navigation’s relevant global object's associated Document's origin is opaque, then return true.

  5. Return false.

To update the entries for a Navigation instance navigation given a NavigationType-or-null navigationTypeForCurrententrychange and a boolean deferNotification (default false):
  1. If navigation has entries and events disabled, then:

    1. Assert: navigation’s entry list is empty.

    2. Return.

  2. Let sessionHistory be navigation’s relevant global object's browsing context's session history.

    It is expected that this include session history entries in the entire browsing session, including those in different browsing context groups due to `Cross-Origin-Opener-Policy`-induced switches. This will be better-defined when whatwg/html#6315 is finalized; see also whatwg/html#6356 for some discussion of the impact of manual navigation on this "session" concept.

    Note that it is OK to expose the data in these entries to the current page through NavigationHistoryEntry instances, since any navigation API state will have been put there affirmatively, and the URL is hidden appropriately by the url getter when the document indicates that its URL is sensitive through the "no-referrer" or "origin" referrer policies.

  3. Let navigationAPISHEs be a new empty list.

  4. Let oldCurrentNHE be the current entry of navigation.

  5. Let currentSHE be sessionHistory’s current entry.

  6. Let backwardIndex be the index of currentSHE within sessionHistory, minus 1.

  7. While backwardIndex > 0:

    1. Let she be sessionHistory[backwardIndex].

    2. If she’s origin is same origin with currentSHE’s origin, then prepend she to navigationAPISHEs.

    3. Otherwise, break.

    4. Set backwardIndex to backwardIndex − 1.

  8. Append currentSHE to navigationAPISHEs.

  9. Let forwardIndex be the index of currentSHE within sessionHistory, plus 1.

  10. While forwardIndex < sessionHistory’s size:

    1. Let she be sessionHistory[forwardIndex].

    2. If she’s origin is same origin with currentSHE’s origin, then append she to navigationAPISHEs.

    3. Otherwise, break.

    4. Set forwardIndex to forwardIndex + 1.

  11. Let newCurrentIndex be the index of currentSHE within navigationAPISHEs.

  12. Let newEntryList be an empty list.

  13. For each oldNHE of navigation’s entry list:

    1. Set oldNHE’s index to −1.

  14. Let index be 0.

  15. Let disposedNHEs be a clone of navigation’s entry list.

  16. For each she of navigationAPISHEs:

    1. If navigation’s entry list contains a NavigationHistoryEntry existingNHE whose session history entry is she, then:

      1. Append existingNHE to newEntryList.

      2. Remove existingNHE from disposedNHEs.

    2. Otherwise:

      1. Let newAHE be a new NavigationHistoryEntry created in the relevant realm of navigation.

      2. Set newAHE’s session history entry to she.

      3. Append newAHE to newEntryList.

    3. Set newEntryList[index]'s index to index.

    4. Set index to index + 1.

  17. Set navigation’s entry list to newEntryList.

  18. Set navigation’s current entry index to newCurrentIndex.

  19. If deferNotification is true, then queue a global task on the navigation and traversal task source given navigation’s relevant global object to run the following steps. Otherwise, proceed onward to run these steps within the current task.

  20. If navigation’s ongoing navigation is non-null, then notify about the committed-to entry given navigation’s ongoing navigation and the current entry of navigation.

    It is important to do this before firing the dispose or currententrychange events, since event handlers could start another navigation, or otherwise change the value of navigation’s ongoing navigation.

  21. If oldCurrentNHE is not null, and oldCurrentNHE does not equal navigation’s current entry, then:

    1. Assert: navigationTypeForCurrententrychange is not null.

    2. Fire an event named currententrychange at navigation using NavigationCurrentEntryChangeEvent, with its navigationType attribute initialized to navigationTypeForCurrententrychange and its from initialized to oldCurrentNHE.

    oldCurrentNHE is null the first time update the entries is run for a Navigation object, i.e. on new Document creation. oldCurrentNHE and navigation’s current entry are equal if we are reactivating a page in the back/forward cache. In both cases, currententrychange does not fire.

  22. For each disposedAHE of disposedNHEs:

    1. Fire an event named dispose at disposedAHE.

To get the navigation API history index of a session history entry she within a Navigation navigation:
  1. Let index be 0.

  2. For each ahe of navigation’s entry list:

    1. If ahe’s session history entry is equal to she, then return index.

    2. Increment index by 1.

  3. Assert: this step is never reached.

1.1. Introspecting the navigation API history entry list

entries = navigation.entries()

Returns an array of NavigationHistoryEntry instances representing the current navigation history entry list, i.e. all session history entries for this Window that are same origin and contiguous to the current session history entry.

navigation.canGoBack

Returns true if the current NavigationHistoryEntry is not the first one in the navigation history entry list.

navigation.canGoForward

Returns true if the current NavigationHistoryEntry is not the last one in the navigation history entry list.

The entries() method steps are:
  1. If this has entries and events disabled, then return the empty list.

  2. Return this's entries list.

The canGoBack getter steps are:
  1. If this has entries and events disabled, then return false.

  2. Assert: this's current entry index is not −1.

  3. If this's current entry index is 0, then return false.

  4. Return true.

The canGoForward getter steps are:
  1. If this has entries and events disabled, then return false.

  2. Assert: this's current entry index is not −1.

  3. If this's current entry index is equal to this's entry list's size − 1, then return false.

  4. Return true.

1.2. The current entry

[Exposed=Window]
interface NavigationCurrentEntryChangeEvent : Event {
  constructor(DOMString type, NavigationCurrentEntryChangeEventInit eventInit);

  readonly attribute NavigationType? navigationType;
  readonly attribute NavigationHistoryEntry from;
};

dictionary NavigationCurrentEntryChangeEventInit : EventInit {
  NavigationType? navigationType = null;
  required NavigationHistoryEntry destination;
};
navigation.currentEntry

The current NavigationHistoryEntry.

navigation.updateCurrentEntry({ state })

Update the navigation API state of the current NavigationHistoryEntry, without performing a navigation like navigation.reload() would do.

This method is best used to capture updates to the page that have already happened, and need to be reflected into the navigation API state. For cases where the state update is meant to drive a page update, instead use navigation.navigate() or navigation.reload().

The current entry for a Navigation navigation is the result running of the following algorithm:
  1. If navigation has entries and events disabled, then return null.

  2. Assert: navigation’s current entry index is not −1.

  3. Return navigation’s entry list[navigation’s current entry index].

The currentEntry getter steps are to return the current entry for this.

The updateCurrentEntry(options) method steps are:
  1. Let current be the current entry for this.

  2. If current is null, then throw an "InvalidStateError" DOMException.

  3. Let serializedState be StructuredSerializeForStorage(options["state"]), rethrowing any exceptions.

  4. Set current’s session history entry's navigation API state to serializedState.

  5. Fire an event named currententrychange at this using NavigationCurrentEntryChangeEvent, with its navigationType attribute initialized to null and its from initialized to current.

1.3. Ongoing navigation tracking

[Exposed=Window]
interface NavigationTransition {
  readonly attribute NavigationType navigationType;
  readonly attribute NavigationHistoryEntry from;
  readonly attribute Promise<undefined> finished;
};
navigation.transition

A NavigationTransition object representing any ongoing navigation that hasn’t yet reached the navigatesuccess or navigateerror stage, if one exists, or null if there is no such transition ongoing.

Since navigation.currentEntry (and other properties like location.href) are updated immediately upon navigation, this navigation.transition property is useful for determining when such navigations are not yet fully settled, according to any handlers passed to event.intercept().

navigation.transition.navigationType

One of "reload", "push", "replace", or "traverse", indicating what type of navigation this transition is for.

navigation.transition.from

The NavigationHistoryEntry from which the transition is coming. This can be useful to compare against navigation.currentEntry.

navigation.transition.finished

A promise which fulfills at the same time the navigatesuccess event fires, or rejects at the same time the navigateerror fires.

A Navigation has a transition, which is a NavigationTransition or null.

The transition getter steps are to return this's transition.


A NavigationTransition has an associated navigation type, which is a NavigationType.

A NavigationTransition has an associated from entry, which is a NavigationHistoryEntry.

A NavigationTransition has an associated finished promise, which is an Promise.

The navigationType getter steps are to return this's navigation type.

The from getter steps are to return this's from entry.

The finished getter steps are to return this's finished promise.


During any given navigation, the Navigation object needs to keep track of the following:

For all navigations
State Duration Explanation
The NavigateEvent For the duration of event firing So that if the navigation is canceled while the event is firing, we can cancel the event.
The event’s signal Until all promises returned from handlers passed to intercept() have settled So that if the navigation is canceled, we can signal abort.
Whether a new element was focused Until all promises returned from handlers passed to intercept() have settled So that if one was, focus is not reset
The NavigationHistoryEntry being navigated to From when it is determined, until all promises returned from handlers passed to intercept() have settled So that we know what to resolve any committed and finished promises with.
Any finished Promise that was returned Until all promises returned from handlers passed to intercept() have settled So that we can resolve or reject it appropriately.
For non-"traverse" navigations
State Duration Explanation
Any state For the duration of event firing So that we can update the current entry’s state after the event successfully finishes firing without being canceled.
For "traverse" navigations
State Duration Explanation
Any info Until the task is queued to fire the navigate event So that we can use it to fire the navigate event after the the trip through the session history traversal queue.
Any committed Promise that was returned Until the session history is updated (inside that same task) So that we can resolve or reject it appropriately.
Whether intercept() was called Until the session history is updated (inside that same task) So that we can suppress the normal scroll restoration logic in favor of the chosen scrollRestoration option value.

Furthermore, we need to account for the fact that there might be multiple traversals queued up, e.g. via

const key1 = navigation.entries()[navigation.currentEntry.index - 1].key;
const key2 = navigation.entries()[navigation.currentEntry.index + 1].key;

navigation.traverseTo(key1); // intentionally no await
navigation.traverseTo(key2);

And, while non-traversal navigations cannot be queued in the same way since a new non-traversal navigation cancels an old one, we need to keep some state around so that we can properly cancel the old one. That is, given

const p1 = navigation.navigate(url1).finished;
const p2 = navigation.navigate(url2).finished;

we need to ensure that when navigating to url2, we still have the Promise p1 around so that we can reject it. We can’t just get rid of any ongoing navigation promises the moment the second call to navigate() happens.

We also need to ensure that, if we start a new navigation, navigations which have gotten as far as firing navigate events, but not yet as far as firing navigatesuccess or navigateerror, get finalized with an aborted navigation error.

We end up accomplishing all this using the following setup:

Each Navigation object has an associated ongoing navigate event, a NavigateEvent or null, initially null.

Each Navigation object has an associated ongoing navigation signal, which is an AbortSignal or null, initially null.

Each Navigation object has an associated focus changed during ongoing navigation, which is a boolean, initially false.

Each Navigation object has an associated suppress normal scroll restoration during ongoing navigation, which is a boolean, initially false.

Each Navigation object has an associated ongoing navigation, which is a navigation API method navigation or null, initially null.

Each Navigation object has an associated upcoming non-traverse navigation, which is a navigation API method navigation or null, initially null.

Each Navigation object has an associated upcoming traverse navigations, which is a map from strings to navigation API method navigations, initially empty.

An navigation API method navigation is a struct with the following items:

We need to store the ongoing navigation signal, focus changed during ongoing navigation, and suppress normal scroll restoration during ongoing navigation separately from the navigation API method navigation struct, since it needs to be tracked even for navigations that are not via the navigation API.

To set the upcoming non-traverse navigation given a Navigation navigation, a JavaScript value info, and a serialized state-or-null serializedState:
  1. Let committedPromise and finishedPromise be new promises created in navigation’s relevant Realm.

  2. Mark as handled finishedPromise.

    The web developer doesn’t necessarily care about finishedPromise being rejected:
    • They might only care about committedPromise.

    • They could be doing multiple synchronous navigations within the same task, in which case all but the last will be aborted (causing their finishedPromise to reject). This could be an application bug, but also could just be an emergent feature of disparate parts of the application overriding each others' actions.

    • They might prefer to listen to other transition-failure signals instead of finishedPromise, e.g., the navigateerror event, or the navigation.transition.finished promise.

    As such, we mark it as handled to ensure that it never triggers unhandledrejection events.

  3. Let ongoingNavigation be a navigation API method navigation whose navigation object is navigation, key is null, info is info, serialized state is serializedState, committed-to entry is null, committed promise is committedPromise, and finished promise is finishedPromise.

  4. Assert: navigation’s upcoming non-traverse navigation is null.

  5. Set navigation’s upcoming non-traverse navigation to ongoingNavigation.

  6. Return ongoingNavigation.

To set an upcoming traverse navigation given a Navigation navigation, a string key, and a JavaScript value info:
  1. Let committedPromise and finishedPromise be new promises created in navigation’s relevant Realm.

  2. Mark as handled finishedPromise.

    See the previous discussion as to why this is done.

  3. Let traversal be a navigation API method navigation whose whose navigation object is navigation, key is key, info is info, serialized state is null, committed-to entry is null, committed promise is committedPromise, and finished promise is finishedPromise.

  4. Set navigation’s upcoming traverse navigations[key] to traversal.

  5. Return traversal.

To promote the upcoming navigation to ongoing given a Navigation navigation and a string-or-null destinationKey:
  1. Assert: navigation’s ongoing navigation is null.

  2. If destinationKey is not null, then:

    1. Assert: navigation’s upcoming non-traverse navigation is null.

    2. If navigation’s upcoming traverse navigations[destinationKey] exists, then:

      1. Set navigation’s ongoing navigation to navigation’s upcoming traverse navigations[destinationKey].

      2. Remove navigation’s upcoming traverse navigations[destinationKey].

  3. Otherwise,

    1. Set navigation’s ongoing navigation to navigation’s upcoming non-traverse navigation.

    2. Set navigation’s upcoming non-traverse navigation to null.

To clean up a navigation API method navigation navigation:
  1. Let navigation be navigation’s navigation object.

  2. If navigation’s ongoing navigation is navigation, then set navigation’s ongoing navigation to null.

  3. Otherwise,

    1. Assert: navigation’s key is not null.

    2. Assert: navigation’s upcoming traverse navigations[navigation’s key] exists.

    3. Remove navigation’s upcoming traverse navigations[navigation’s key].

To notify about the committed-to entry given a navigation API method navigation navigation and a NavigationHistoryEntry entry:
  1. Set navigation’s committed-to entry to entry.

  2. If navigation’s serialized state is not null, then set entry’s session history entry's navigation API state to navigation’s serialized state.

    If it’s null, then we’re traversing to entry via traverseTo(), which does not allow changing the state.

    After this point, navigation’s serialized state is no longer needed. Implementations might want to clear it out to avoid keeping it alive for the lifetime of the navigation API method navigation.

  3. Resolve navigation’s committed promise with entry.

    After this point, navigation’s committed promise is only needed in cases where it has not yet been returned to author code. Implementations might want to clear it out to avoid keeping it alive for the lifetime of the navigation API method navigation.

To resolve the finished promise for a navigation API method navigation navigation:
  1. If navigation’s finished promise is null, then return.

  2. Resolve navigation’s finished promise with its committed-to entry.

  3. Clean up navigation.

To reject the finished promise for a navigation API method navigation navigation with a JavaScript value exception:
  1. If navigation’s finished promise is null, then return.

  2. If navigation’s committed promise is not null, then reject navigation’s committed promise with exception.

  3. Reject navigation’s finished promise with exception.

  4. Clean up navigation.

{ committed, finished } = navigation.navigate(url)
{ committed, finished } = navigation.navigate(url, options)

Navigates the current page to the given url. options can contain the following values:

  • history can be set to "replace" to replace the current session history entry, instead of pushing a new one.

  • info can be set to any value; it will populate the info property of the corresponding navigate event.

  • state can be set to any serializable value; it will populate the state retrieved by navigation.currentEntry.getState() once the navigation completes, for same-document navigations. (It will be ignored for navigations that end up cross-document.)

By default this will perform a full navigation (i.e., a cross-document navigation, unless the given URL differs only in a fragment from the current one). The navigate event’s intercept() method can be used to convert it into a same-document navigation.

The returned promises will behave as follows:

  • For navigations that get aborted, both promises will reject with an "AbortError" DOMException.

  • For same-document navigations created by using the navigate event’s intercept() method, committed will fulfill immediately, and finished will fulfill or reject according to any promises returned by handlers passed to intercept().

  • For other same-document navigations (e.g., non-intercepted fragment navigations, both promises will fulfill immediately.

  • For cross-document navigations, or navigations that result in 204/205 statuses or Content-Disposition: attachment header fields from the server (and thus do not actually navigate), both promises will never settle.

In all cases, when the returned promises fulfill, it will be with the NavigationHistoryEntry that was navigated to.

{ committed, finished } = navigation.reload(options)

Reloads the current page. The info and state options behave as described above.

The default behavior of performing a from-network-or-cache reload of the current page can be overriden by using the navigate event’s intercept() method. Doing so will mean this call only updates state or passes along the appropriate info, plus performing whatever actions the navigate event handler sees fit to carry out.

The returned promises will behave as follows:

The navigate(url, options) method steps are:
  1. Parse url relative to this's relevant settings object. If that returns failure, then return an early error result for a "SyntaxError" DOMException. Otherwise, let urlRecord be the resulting URL record.

  2. Let document be this's relevant global object's associated document.

  3. If options["history"] is "push", and any of the following are true:

    then return an early error result for a "NotSupportedError" DOMException.

    These are the conditions under which a push navigation will be converted into a replace navigation by the navigate algorithm or by the below step. If the developer explicitly requested a push, we fail to let them know it won’t happen.

    In the future, we could consider loosening some of these conditions, e.g., allowing explicitly-requested push navigations to the current URL or before the document is completely loaded.

  4. Let state be options["state"] if it exists; otherwise, undefined.

  5. Let serializedState be StructuredSerializeForStorage(state). If this throws an exception, then return an early error result for that exception.

    It is important to perform this step early, since serialization can invoke web developer code, which in turn might change the state checked in later steps.

  6. Let info be options["info"] if it exists; otherwise, undefined.

  7. Let historyHandling be "replace" if options["history"] is "replace" or if document is not completely loaded; otherwise, "default".

  8. Return the result of performing a non-traverse navigation API navigation given this, urlRecord, serializedState, info, and historyHandling.

The reload(options) method steps are:
  1. Let urlRecord be this's relevant global object's active document's URL.

  2. Let serializedState be null.

  3. If options["state"] exists, then set serializedState to StructuredSerializeForStorage(options["state"]). If this throws an exception, then return an early error result for that exception.

    It is important to perform this step early, since serialization can invoke web developer code, which in turn might change the state checked in later steps.

  4. Otherwise,

    1. Let current be the current entry of this.

    2. If current is not null, then set serializedState to current’s navigation API state.

    3. Otherwise, set serializedState to StructuredSerializeForStorage(undefined).

  5. Let info be options["info"] if it exists; otherwise, undefined.

  6. Return the result of performing a non-traverse navigation API navigation given this, urlRecord, serializedState, info, and "reload".

To perform a non-traverse navigation API navigation given a Navigation object navigation, a URL url, a serialized state serializedState, a JavaScript value info, and a history handling behavior historyHandling:
  1. If navigation’s relevant global object's associated Document is not fully active, then return an early error result for an "InvalidStateError" DOMException.

  2. If navigation’s relevant global object's associated Document's unload counter is greater than 0, then return an early error result for an "InvalidStateError" DOMException.

  3. Let browsingContext be navigation’s relevant global object's browsing context.

  4. Assert: browsingContext is not null.

  5. Assert: historyHandling is either "replace", "reload", or "default".

  6. Let ongoingNavigation be the result of setting the upcoming non-traverse navigation for navigation given info.

  7. Navigate browsingContext to url with historyHandling set to historyHandling, navigationAPIState set to serializedState, and the source browsing context set to browsingContext.

  8. If navigation’s upcoming non-traverse navigation is ongoingNavigation, then:

    This means the navigate algorithm bailed out before ever getting to the inner navigate event firing algorithm which would promote the upcoming navigation to ongoing.

    1. Set navigation’s upcoming non-traverse navigation to null.

    2. Return an early error result for an "AbortError" DOMException.

  9. Return «[ "committed" → ongoingNavigation’s committed promise, "finished" → ongoingNavigation’s finished promise ]».

Unlike location.assign() and friends, which are exposed across origin-domain boundaries, navigation.navigate() and navigation.reload() can only be accessed by code with direct synchronous access to the navigation property. Thus, we avoid the complications around tracking source browsing contexts, and we don’t need to deal with the allowed to navigate check and its accompanying exceptionsEnabled flag. We just treat all navigations as being initiated by the Navigation object itself.

An an early error result for an exception e is a dictionary instance given by «[ "committed" → a promise rejected with e, "finished" → a promise rejected with e ]».

1.5. Traversing

{ committed, finished } = navigation.traverseTo(key)
{ committed, finished } = navigation.traverseTo(key, { info })

Traverses the joint session history to the closest joint session history entry that matches the NavigationHistoryEntry with the given key. info can be set to any value; it will populate the info property of the corresponding navigate event.

If a traversal to that joint session history is already in progress, then this will return the promises for that original traversal, and info will be ignored.

The returned promises will behave as follows:

{ committed, finished } = navigation.back()
{ committed, finished } = navigation.back({ info })

Traverse the joint session history to the closest previous joint session history entry which results in this frame navigating, i.e. results in navigation.currentEntry updating. info can be set to any value; it will populate the info property of the corresponding navigate event.

If a traversal to that joint session history is already in progress, then this will return the promises for that original traversal, and info will be ignored.

The returned promises behave equivalently to those returned by traverseTo().

{ committed, finished } = navigation.forward()
{ committed, finished } = navigation.forward({ info })

Traverse the joint session history to the closest forward joint session history entry which results in this frame navigating, i.e. results in navigation.currentEntry updating. info can be set to any value; it will populate the info property of the corresponding navigate event.

If a traversal to that joint session history is already in progress, then this will return the promises for that original traversal, and info will be ignored.

The returned promises behave equivalently to those returned by traverseTo().

The traverseTo(key, options) method steps are:
  1. If this's current entry index is −1, then return an early error result for an "InvalidStateError" DOMException.

  2. If this's entry list does not contain any NavigationHistoryEntry whose session history entry's navigation API key equals key, then return an early error result for an "InvalidStateError" DOMException.

  3. Return the result of performing a navigation API traversal given this, key, and options.

The back(options) method steps are:
  1. If this's current entry index is −1 or 0, then return an early error result for an "InvalidStateError" DOMException.

  2. Let key be this's entry list[this's current entry index − 1]'s session history entry's navigation API key.

  3. Return the result of performing a navigation API traversal given this, key, and options.

The forward(options) method steps are:
  1. If this's current entry index is −1 or is equal to this's entry list's size − 1, then return an early error result for an "InvalidStateError" DOMException.

  2. Let key be this's entry list[this's current entry index + 1]'s session history entry's navigation API key.

  3. Return the result of performing a navigation API traversal given this, key, and options.

The following algorithm is specified in terms of the session history rewrite pull request against the HTML Standard, because the existing session history traversal infrastructure is broken enough that it’s hard to build on. It is expected to track that work as it continues.

To perform a navigation API traversal given a Navigation object navigation, a string key, and a NavigationOptions options:

  1. Let sourceDocument be navigation’s relevant global object's associated Document.

  2. If sourceDocument is not fully active, then return an early error result for an "InvalidStateError" DOMException.

  3. If sourceDocument’s unload counter is greater than 0, then return an early error result for an "InvalidStateError" DOMException.

  4. If navigation’s current entry's session history entry's navigation API key equals key, then return «[ "committed" → a promise resolved with navigation’s current entry, "finished" → a promise resolved with navigation’s current entry

  5. If navigation’s upcoming traverse navigations[key] exists, then:

    1. Let navigation be navigation’s upcoming traverse navigations[key].

    2. Return «[ "committed" → navigation’s committed promise, "finished" → navigation’s finished promise ]».

  6. Let info be options["info"] if it exists, or undefined otherwise.

  7. Let ongoingNavigation be the result of setting an upcoming traverse navigation for navigation given key and info.

  8. Let navigable be sourceDocument’s node navigable.

  9. Let traversable be navigable’s traversable navigable.

  10. Let unsafeNavigationStartTime be the unsafe shared current time.

  11. Let sourceSnapshotParams be the result of snapshotting source snapshot params given sourceDocument.

  12. Append the following session history traversal steps to traversable:

    1. Let navigableEntries be the result of getting the session history entries given navigable.

    2. Let targetEntry be the session history entry in navigableEntries whose navigation API key equals key. If no such entry exists, then:

      1. Queue a global task on the navigation and traversal task source given sourceDocument’s relevant global object to reject the finished promise for ongoingNavigation with an "InvalidStateError" DOMException.

      2. Abort these steps.

      This can occur if the navigation object’s view of session history is outdated, which can happen for brief periods while all the relevant threads and processes are being synchronized in reaction to a history change (such as the user clearing their history).

    3. If targetEntry is navigable’s active session history entry, then abort these steps.

      This can occur if a previously-queued-up traversal already took us to this session history entry. In that case that previous traversal will have dealt with ongoingNavigation already.

    4. Apply the history step given by targetEntry’s step to traversable, with checkForUserCancellation set to true, unsafeNavigationStartTime set to unsafeNavigationStartTime, sourceSnapshotParams set to sourceSnapshotParams, and initiatorToCheck set to navigable.

      As part of merging this spec into HTML, we would modify apply the history step to have well-specified hooks for communicating these conditions back to its caller.

  13. Return «[ "committed" → ongoingNavigation’s committed promise, "finished" → ongoingNavigation’s finished promise ]».

1.6. Event handlers

The following are the event handlers (and their corresponding event handler event types) that must be supported, as event handler IDL attributes, by objects implementing the Navigation interface:

Event handler Event handler event type
onnavigate navigate
onnavigatesuccess navigatesuccess
onnavigateerror navigateerror
oncurrententrychange currententrychange
[Exposed=Window]
interface NavigateEvent : Event {
  constructor(DOMString type, NavigateEventInit eventInit);

  readonly attribute NavigationType navigationType;
  readonly attribute NavigationDestination destination;
  readonly attribute boolean canIntercept;
  readonly attribute boolean userInitiated;
  readonly attribute boolean hashChange;
  readonly attribute AbortSignal signal;
  readonly attribute FormData? formData;
  readonly attribute DOMString? downloadRequest;
  readonly attribute any info;

  undefined intercept(optional NavigationInterceptOptions options = {});
  undefined restoreScroll();
};

dictionary NavigateEventInit : EventInit {
  NavigationType navigationType = "push";
  required NavigationDestination destination;
  boolean canIntercept = false;
  boolean userInitiated = false;
  boolean hashChange = false;
  required AbortSignal signal;
  FormData? formData = null;
  DOMString? downloadRequest = null;
  any info;
};

dictionary NavigationInterceptOptions {
  NavigationInterceptHandler handler;
  NavigationFocusReset focusReset;
  NavigationScrollRestoration scrollRestoration;
};

enum NavigationFocusReset {
  "after-transition",
  "manual"
};

enum NavigationScrollRestoration {
  "after-transition",
  "manual"
};

callback NavigationInterceptHandler = Promise<undefined> ();

enum NavigationType {
  "reload",
  "push",
  "replace",
  "traverse"
};
event.navigationType

One of "reload", "push", "replace", or "traverse", indicating what type of navigation this is.

event.destination

A NavigationDestination representing the destination of the navigation.

event.canIntercept

True if intercept() can be called to intercept this navigation and convert it into a single-page navigation; false otherwise.

Generally speaking, this will be true whenever the current Document can have its URL rewritten to the destination URL, except for cross-document back/forward navigations, where it will always be false.

event.userInitiated

True if this navigation was due to a user clicking on an a element, submitting a form element, or using the browser UI to navigate; false otherwise.

event.hashChange

True if this navigation is a fragment navigation; false otherwise.

event.signal

An AbortSignal which will become aborted if the navigation gets canceled, e.g. by the user pressing their browser’s "Stop" button, or another higher-priority navigation interrupting this one.

The expected pattern is for developers to pass this along to any async operations, such as fetch(), which they perform as part of handling this navigation.

event.formData

The FormData representing the submitted form entries for this navigation, if this navigation is a "push" or "replace" navigation representing a POST form submission; null otherwise.

(Notably, this will be null even for "reload" and "traverse" navigations that are revisiting a session history entry that was originally created from a form submission.)

event.downloadRequest

Represents whether or not this navigation was requested to be a download, by using an a or area element’s download attribute:

  • If a download was not requested, then this property is null.

  • If a download was requested, returns the filename that was supplied, via <a download="filename" href="...">. (This could be the empty string, as in the case of <a download href="...">.)

Note that a download being requested does not always mean that a download will happen: for example, the download might be blocked by browser security policies, or end up being treated as a push navigation for unspecified reasons.

Similarly, a navigation might end up being a download even if it was not requested to be one, due to the destination server responding with a Content-Disposition: attachment header.

Finally, note that the navigate event will not fire at all for downloads initiated using browser UI affordances, e.g., those created by right-clicking and choosing to save the target of the link.

event.info

An arbitrary JavaScript value passed via navigation APIs that initiated this navigation, or null if the navigation was initiated by the user or via a non-navigation API.

event.intercept({ handler, focusReset, scrollRestoration })

Intercepts this navigation, preventing its normally handling and instead converting it into a same-document navigation to the destination URL.

The handler option can be a function that returns a promise. The handler function will run after the navigate event has finished firing, and the navigation.currentEntry property has been synchronously updated. This promise is used to signal the duration, and success or failure, of the navigation. After it settles, the browser signals to the user (e.g. via a loading spinner UI, or assistive technology) that the navigation is finished. Additionally, it fires navigatesuccess or navigateerror events as appropriate, which other parts of the web application can respond to.

By default, using this method will cause focus to reset when any handlers' returned promises settle. Focus will be reset to the first element with the autofocus attribute set, or the body element if the attribute isn’t present. The focusReset option can be set to "manual" to avoid this behavior.

By default, using this method for "traverse" navigations will cause the browser’s scroll restoration logic to be delayed until any handlers' returned promises settle. The scrollRestoration option can be set to "manual" to turn off scroll restoration entirely for this navigation, or control the timing of it by later calling restoreScroll().

This method will throw a "SecurityError" DOMException if canIntercept is false, or if isTrusted is false. It will throw an "InvalidStateError" DOMException if not called synchronously, during event dispatch.

event.restoreScroll()

For "traverse" navigations which have set scrollRestoration: "manual" as part of their intercept() call, restores the scroll position using the browser’s usual scroll restoration logic.

If used on a non-"traverse" navigation, or on one which has not had scrollRestoration set appropriately, or if called more than once, this method will throw an "InvalidStateError" DOMException.

The navigationType, destination, canIntercept, userInitiated, hashChange, signal, formData, downloadRequest, and info getter steps are to return the value that the corresponding attribute was initialized to.

A NavigateEvent has a classic history API serialized data, a serialized state-or-null. It is only used in some cases where the event’s navigationType is "push" or "replace", and is set appropriately when the event is fired.

A NavigateEvent has a focus reset behavior, a NavigationFocusReset-or-null, initially null.

A NavigateEvent has a scroll restoration behavior, a NavigationScrollRestoration-or-null, initially null.

A NavigateEvent has a did process scroll restoration, a boolean, initially false.

A NavigateEvent has a was intercepted, a boolean, initially false.

A NavigateEvent has a navigation handler list, which is a list of NavigationInterceptHandler callbacks, initially empty.

The intercept(options) method steps are:
  1. If this's relevant global object's active Document is not fully active, then throw an "InvalidStateError" DOMException.

  2. If this's isTrusted attribute was initialized to false, then throw a "SecurityError" DOMException.

  3. If this's canIntercept attribute was initialized to false, then throw a "SecurityError" DOMException.

  4. If this's dispatch flag is unset, then throw an "InvalidStateError" DOMException.

  5. If this's canceled flag is set, then throw an "InvalidStateError" DOMException.

  6. If options["handler"] exists, then append it to this's navigation handler list.

  7. Set this's was intercepted to true.

  8. If options["focusReset"] exists, then:

    1. If this's focus reset behavior is not null, and it is not equal to options["focusReset"], then the user agent may report a warning to the console indicating that the focusReset option for a previous call to intercept() was overridden by this new value, and the previous value will be ignored.

    2. Set this's focus reset behavior to options["focusReset"].

  9. If options["scrollRestoration"] exists, and this's navigationType attribute was initialized to "traverse", then:

    1. If this's scroll restoration behavior is not null, and it is not equal to options["scrollRestoration"], then the user agent may report a warning to the console indicating that the scrollRestoration option for a previous call to intercept() was overridden by this new value, and the previous value will be ignored.

    2. Set this's scroll restoration behavior to options["scrollRestoration"].

The restoreScroll() method steps are:
  1. If this's navigationType was not initialized to "traverse", then throw an "InvalidStateError" DOMException.

  2. If this's scroll restoration behavior is not "manual", then throw an "InvalidStateError" DOMException.

  3. If this's did process scroll restoration is true, then throw an "InvalidStateError" DOMException.

  4. Restore scroll position data given this's relevant global object's navigable's active session history entry.

[Exposed=Window]
interface NavigationDestination {
  readonly attribute USVString url;
  readonly attribute DOMString? key;
  readonly attribute DOMString? id;
  readonly attribute long long index;
  readonly attribute boolean sameDocument;

  any getState();
};
event.destination.url

The URL being navigated to.

event.destination.key

The value of the key property of the destination NavigationHistoryEntry, if this is a "traverse" navigation, or null otherwise.

event.destination.id

The value of the id property of the destination NavigationHistoryEntry, if this is a "traverse" navigation, or null otherwise.

event.destination.index

The value of the index property of the destination NavigationHistoryEntry, if this is a "traverse" navigation, or −1 otherwise.

event.destination.sameDocument

Indicates whether or not this navigation is to the same Document as the current document value, or not. This will be true, for example, in cases of fragment navigations or history.pushState() navigations.

Note that this property indicates the original nature of the navigation. If a cross-document navigation is converted into a same-document navigation using event.intercept(), that will not change the value of this property.

state = event.destination.getState()

For "traverse" navigations, returns the deserialization of the state stored in the destination session history entry.

For "push" and "replace" navigations, returns the deserialization of the state passed to navigation.navigate(), if the navigation was initiated in that way, or undefined if it wasn’t.

For "reload" navigations, returns the deserialization of the state passed to navigation.reload(), if the reload was initiated in that way, or undefined if it wasn’t.

A NavigationDestination has an associated URL, which is a URL.

A NavigationDestination has an associated key, which is a string-or-null.

A NavigationDestination has an associated id, which is a string-or-null.

A NavigationDestination has an associated index, which is an integer.

A NavigationDestination has an associated state, which is a serialized state-or-null.

A NavigationDestination has an associated is same document, which is a boolean.

The url getter steps are to return this's URL, serialized.

The key getter steps are to return this's key.

The id getter steps are to return this's id.

The index getter steps are to return this's index.

The sameDocument getter steps are to return this's is same document.

The getState() method steps are:
  1. If this's state is null, then return undefined.

  2. Return StructuredDeserialize(this's state).

To fire a traversal navigate event at a Navigation navigation given a session history entry destinationEntry, and an optional user navigation involvement userInvolvement (default "none"):
  1. Let event be the result of creating an event given NavigateEvent, in navigation’s relevant Realm.

  2. Set event’s classic history API serialized data to null.

  3. Let destination be a new NavigationDestination created in navigation’s relevant Realm.

  4. Set destination’s URL to destinationEntry’s URL.

  5. If destinationEntry’s origin is same origin with navigation’s relevant settings object's origin, then:

    1. Set destination’s key to destinationEntry’s navigation API key.

    2. Set destination’s id to destinationEntry’s navigation API ID.

    3. Set destination’s index to the result of getting the navigation API history index of destinationEntry within navigation.

    4. Set destination’s state to destinationEntry’s navigation API state.

  6. Otherwise,

    1. Set destination’s key to null.

    2. Set destination’s id to null.

    3. Set destination’s index to −1.

    4. Set destination’s state to null.

  7. Set destination’s is same document to true if destinationEntry’s document is equal to navigation’s relevant global object's associated Document; otherwise false.

  8. Let result be the result of performing the inner navigate event firing algorithm given navigation, "traverse", event, destination, userInvolvement, null, and null.

  9. Assert: result is true (traversals are never cancelable).

To fire a non-traversal navigate event at a Navigation navigation given a NavigationType navigationType, a URL destinationURL, a boolean isSameDocument, an optional user navigation involvement userInvolvement (default "none"), an optional serialized state-or-null state (default null), an optional entry list or null formDataEntryList (default null), and an optional serialized state-or-null classicHistoryAPISerializedData (default null):
  1. Let event be the result of creating an event given NavigateEvent, in navigation’s relevant Realm.

  2. Set event’s classic history API serialized data to classicHistoryAPISerializedData.

  3. Let destination be a new NavigationDestination created in navigation’s relevant Realm.

  4. Set destination’s URL to destinationURL.

  5. Set destination’s key to null.

  6. Set destination’s id to null.

  7. Set destination’s index to −1.

  8. Set destination’s state to state.

  9. Set destination’s is same document to isSameDocument.

  10. Return the result of performing the inner navigate event firing algorithm given navigation, navigationType, event, destination, userInvolvement, formDataEntryList, and null.

To fire a download-requested navigate event at a Navigation navigation given a URL destinationURL, a user navigation involvement userInvolvement, and a string filename:
  1. Let event be the result of creating an event given NavigateEvent, in navigation’s relevant Realm.

  2. Set event’s classic history API serialized data to null.

  3. Let destination be a new NavigationDestination created in navigation’s relevant Realm.

  4. Set destination’s URL to destinationURL.

  5. Set destination’s key to null.

  6. Set destination’s id to null.

  7. Set destination’s index to −1.

  8. Set destination’s state to null.

  9. Set destination’s is same document to false.

  10. Return the result of performing the inner navigate event firing algorithm given navigation, "push", event, destination, userInvolvement, null, and filename.

The inner navigate event firing algorithm is the following steps, given a Navigation navigation, a NavigationType navigationType, a NavigateEvent event, a NavigationDestination destination, a user navigation involvement userInvolvement, an entry list or null formDataEntryList, and a string or null downloadRequestFilename:
  1. Promote the upcoming navigation to ongoing given navigation and destination’s key.

  2. Let ongoingNavigation be navigation’s ongoing navigation.

  3. If navigation has entries and events disabled, then:

    1. If ongoingNavigation is not null, then clean up ongoingNavigation.

      In this case the committed promise and finished promise will never fulfill, since we never create NavigationHistoryEntrys for the initial about:blank Document so we have nothing to resolve them with.

    2. Return true.

  4. Let document be navigation’s relevant global object's associated document.

  5. If document can have its URL rewritten to destination’s URL, and either destination’s is same document is true or navigationType is not "traverse", then initialize event’s canIntercept to true. Otherwise, initialize it to false.

  6. If navigationType is not "traverse", then initialize event’s cancelable to true. Otherwise, initialize it to false.

  7. Initialize event’s type to "navigate".

  8. Initialize event’s navigationType to navigationType.

  9. Initialize event’s destination to destination.

  10. Initialize event’s downloadRequest to downloadRequestFilename.

  11. If ongoingNavigation is not null, then initialize event’s info to ongoingNavigation’s info. Otherwise, initialize it to undefined.

    At this point ongoingNavigation’s info is no longer needed and can be nulled out instead of keeping it alive for the lifetime of the navigation API method navigation.

  12. Initialize event’s signal to a new AbortSignal created in navigation’s relevant Realm.

  13. Let currentURL be document’s URL.

  14. If all of the following are true:

    then initialize event’s hashChange to true. Otherwise, initialize it to false.

  15. If userInvolvement is not "none", then initialize event’s userInitiated to true. Otherwise, initialize it to false.

  16. If formDataEntryList is not null, then initialize event’s formData to a new FormData created in navigation’s relevant Realm, associated to formDataEntryList. Otherwise, initialize it to null.

  17. Assert: navigation’s ongoing navigate event is null.

  18. Set navigation’s ongoing navigate event to event.

  19. Assert: navigation’s ongoing navigation signal is null.

  20. Set navigation’s ongoing navigation signal to event’s signal.

  21. Set navigation’s focus changed during ongoing navigation to false.

  22. Set navigation’s suppress normal scroll restoration during ongoing navigation to false.

  23. Let dispatchResult be the result of dispatching event at navigation.

  24. Set navigation’s ongoing navigate event to null.

  25. If dispatchResult is false:

    1. If navigationType is not "traverse" and event’s signal is not aborted, then finalize with an aborted navigation error given navigation and ongoingNavigation.

      If navigationType is "traverse", then we will finalize with an aborted navigation error in perform a navigation API traversal.

    2. Return false.

  26. Let endResultIsSameDocument be true if event’s was intercepted is true or destination’s is same document is true.

  27. If event’s was intercepted is true:

    1. Let fromEntry be the current entry for navigation.

    2. Assert: fromEntry is not null.

    3. Set navigation’s transition to a new NavigationTransition created in navigation’s relevant Realm, whose navigation type is navigationType, from entry is fromEntry, and whose finished promise is a new promise created in navigation’s relevant Realm.

    4. Mark as handled navigation’s transition's finished promise.

      See the discussion about other finished promises as to why this is done.

    5. If navigationType is "traverse", then set navigation’s suppress normal scroll restoration during ongoing navigation to true.

      If event’s scroll restoration behavior was set to "after-transition", then we will potentially perform scroll restoration below. Otherwise, there will be no scroll restoration. That is, no navigation which is intercepted by intercept() goes through the normal scroll restoration process; scroll restoration for such navigations is either done manually, by the web developer, or is done after the transition.

    6. If navigationType is "push" or "replace", then run the URL and history update steps given document and event’s destination's URL, with serializedData set to event’s classic history API serialized data and historyHandling set to navigationType.

      If navigationType is "reload", then we are converting a reload into a "same-document reload", for which the URL and history update steps are not appropriate. Navigation API-related stuff still happens, such as updating the current entry's navigation API state if this was caused by a call to navigation.reload(), and all the ongoing navigation tracking.

  28. If endResultIsSameDocument is true:

    1. Let promisesList be an empty list.

    2. For each handler of event’s navigation handler list:

      1. Append the result of invoking handler to promisesList with an empty arguments list.

    3. If promisesList’s size is 0, then set promisesList to « a promise resolved with undefined ».

      There is a subtle timing difference between how waiting for all schedules its success and failure steps when given zero promises versus ≥1 promises. For most uses of waiting for all, this does not matter. However, with this API, there are so many events and promise handlers which could fire around the same time that the difference is pretty easily observable: it can cause the event/promise handler sequence to vary. (Some of the events and promises involved include: navigatesuccess / navigateerror, currententrychange, dispose, ongoingNavigation’s promises, and the navigation.transition.finished promise.)

    4. Wait for all of promisesList, with the following success steps:

      1. If event’s signal is aborted, then abort these steps.

      2. Fire an event named navigatesuccess at navigation.

      3. If navigation’s transition is not null, then resolve navigation’s transition's finished promise with undefined.

      4. Set navigation’s transition to null.

      5. If ongoingNavigation is non-null, then resolve the finished promise for ongoingNavigation.

      6. Potentially reset the focus given navigation and event.

      7. Potentially perform scroll restoration given navigation and event.

      and the following failure steps given reason rejectionReason:
      1. If event’s signal is aborted, then abort these steps.

      2. Fire an event named navigateerror at navigation using ErrorEvent, with error initialized to rejectionReason, and message, filename, lineno, and colno initialized to appropriate values that can be extracted from rejectionReason in the same underspecified way the user agent typically does for the report an exception algorithm.

      3. If navigation’s transition is not null, then reject navigation’s transition's finished promise with rejectionReason.

      4. Set navigation’s transition to null.

      5. If ongoingNavigation is non-null, then reject the finished promise for ongoingNavigation with rejectionReason.

      6. Potentially reset the focus given navigation and event.

        Although we still potentially reset the focus for such failed transitions, we do not potentially perform scroll restoration for them.

  29. Otherwise, if ongoingNavigation is non-null, then clean up ongoingNavigation.

  30. If event’s was intercepted is true and navigationType is "push", "replace", or "reload", then return false.

  31. Return true.

To finalize with an aborted navigation error given a Navigation navigation, a navigation API method navigation or null ongoingNavigation, and an optional DOMException error:
  1. Set navigation’s focus changed during ongoing navigation to false.

  2. Set navigation’s suppress normal scroll restoration during ongoing navigation to false.

  3. If error was not given, then set error to a new "AbortError" DOMException, created in navigation’s relevant Realm.

  4. If navigation’s ongoing navigate event is non-null, then:

    1. Set navigation’s ongoing navigate event's canceled flag to true.

    2. Set navigation’s ongoing navigate event to null.

  5. If navigation’s ongoing navigation signal is non-null, then:

    1. Signal abort on navigation’s ongoing navigation signal given error.

    2. Set navigation’s ongoing navigation signal to null.

  6. Fire an event named navigateerror at navigation using ErrorEvent, with error initialized to error, and message, filename, lineno, and colno initialized to appropriate values that can be extracted from error and the current JavaScript stack in the same underspecified way the user agent typically does for the report an exception algorithm.

    Thus, for example, if this algorithm is reached because of a call to window.stop(), these properties would probably end up initialized based on the line of script that called window.stop(). But if it’s because the user clicked the stop button, these properties would probably end up with default values like the empty string or 0.

  7. If ongoingNavigation is non-null, then reject the finished promise for ongoingNavigation with error.

  8. If navigation’s transition is not null, then:

    1. Reject navigation’s transition's finished promise with error.

    2. Set navigation’s transition to null.

To inform the navigation API about canceling navigation in a browsing context bc:
  1. Let navigation be bc’s active window's navigation API.

  2. If navigation’s ongoing navigation signal is null, then return.

  3. Finalize with an aborted navigation error given navigation and navigation’s ongoing navigation.

To inform the navigation API about browsing context discarding given a browsing context bc:
  1. Inform the navigation API about canceling navigation in bc.

  2. Let navigation be bc’s active window's navigation API.

  3. Let traversals be a clone of navigation’s upcoming traverse navigations.

  4. For each traversal of traversals: finalize with an aborted navigation error given navigation and traversal.

To potentially reset the focus given a Navigation object navigation and an NavigateEvent event:
  1. Let focusChanged be navigation’s focus changed during ongoing navigation.

  2. Set navigation’s focus changed during ongoing navigation to false.

  3. If focusChanged is true, then return.

  4. If event’s was intercepted is false, then return.

  5. If event’s focus reset behavior is "manual", then return.

    If it was left as null, then we treat that as "after-transition", and continue onward.

  6. Let document be navigation’s relevant global object's associated Document.

  7. Let focusTarget be the autofocus delegate for document.

  8. If focusTarget is null, then set focusTarget to document’s body element.

  9. If focusTarget is null, then set focusTarget to document’s document element.

  10. Run the focusing steps for focusTarget, with document’s viewport as the fallback target.

  11. Move the sequential focus navigation starting point to focusTarget.

To potentially perform scroll restoration given a Navigation object navigation and an NavigateEvent event:
  1. If event’s was intercepted is false, then return.

  2. If event’s navigationType was not initialized to "traverse", then return.

  3. If event’s scroll restoration behavior is "manual", then return.

    If it was left as null, then we treat that as "after-transition", and continue onward.

  4. If event’s did process scroll restoration is true, then return.

  5. Set event’s did process scroll restoration to true.

  6. Restore scroll position data given navigation’s current entry's session history entry.

[Exposed=Window]
interface NavigationHistoryEntry : EventTarget {
  readonly attribute USVString? url;
  readonly attribute DOMString key;
  readonly attribute DOMString id;
  readonly attribute long long index;
  readonly attribute boolean sameDocument;

  any getState();

  attribute EventHandler ondispose;
};
entry.url

The URL of this navigation history entry.

This can return null if the entry corresponds to a different Document than the current one (i.e. if sameDocument is false), and that Document's referrer policy was "no-referrer" or "origin", since that indicates the Document in question is hiding its URL even from other same-origin pages.

entry.key

A user agent-generated random UUID string representing this navigation history entry’s place in the navigation history list. This value will be reused by other NavigationHistoryEntry instances that replace this one due to replace-style navigations. This value will survive session restores.

This is useful for navigating back to this entry in the navigation history list, using navigation.traverseTo(key).

entry.id

A user agent-generated random UUID string representing this specific navigation history entry. This value will not be reused by other NavigationHistoryEntry instances. This value will survive session restores.

This is useful for associating data with this navigation history entry using other storage APIs.

entry.index

The index of this navigation history entry within navigation.entries(), or −1 if the entry is not in the navigation history entry list.

entry.sameDocument

Indicates whether or not this navigation history entry is for the same Document as the current document value, or not. This will be true, for example, when the entry represents a fragment navigation or single-page app navigations.

state = entry.getState()

Returns the deserialization of the state stored in this entry, which was added to the entry using navigation.navigate(). This state survives session restores.

Note that in general, unless the state value is a primitive, entry.getState() !== entry.getState(), since a fresh copy is returned each time.

This state is unrelated to the classic history API’s history.state.

Each NavigationHistoryEntry has an associated session history entry, which is a session history entry.

Each NavigationHistoryEntry has an associated index, which is an integer.

The key getter steps are:
  1. If this's relevant global object's associated Document is not fully active, then return the empty string.

  2. Return this's session history entry's navigation API key.

The id getter steps are:
  1. If this's relevant global object's associated Document is not fully active, then return the empty string.

  2. Return this's session history entry's navigation API ID.

The url getter steps are:
  1. If this's relevant global object's associated Document is not fully active, then return null.

  2. Let she be this's session history entry.

  3. If she’s document does not equal this's relevant global object's associated Document, and she’s policy container's referrer policy is "no-referrer" or "origin", then return null.

  4. Return she’s URL, serialized.

The index getter steps are:
  1. If this's relevant global object's associated Document is not fully active, then return −1.

  2. Return this's index.

The sameDocument getter steps are:
  1. If this's relevant global object's associated Document is not fully active, then return false.

  2. Return true if this's session history entry's document equals this's relevant global object's associated Document, and false otherwise.

The getState() method steps are:
  1. If this's relevant global object's associated Document is not fully active, then return undefined.

  2. If this's session history entry's navigation API state is null, then return undefined.

  3. Return StructuredDeserialize(this's session history entry's navigation API state).

Unlike history.state, this will deserialize upon each access.

This can in theory throw an exception, if attempting to deserialize a large ArrayBuffer when not enough memory is available.

The following are the event handlers (and their corresponding event handler event types) that must be supported, as event handler IDL attributes, by objects implementing the NavigationHistoryEntry interface:

Event handler Event handler event type
ondispose dispose

The following section details monkeypatches to [HTML] that cause the navigate event to be fired appropriately, and for canceling the event to cancel the navigation. The first few sections detail slight tweaks to existing algorithms to pass through useful information into the navigation and history traversal algorithms. Then, § 4.3 Navigation algorithm updates contains the actual firing of the event.

4.1. Form submission patches

To properly thread the form entry list from its creation through to NavigateEvent's formData property, we need the following modifications:

Modify the navigate algorithm to take an entry list or null entryList (default null), replacing its navigationType parameter. Then insert a step somewhere early in the algorithm to convert this back into the navigationType variable used by the in parallel section that is ultimately passed to [CSP]:
  1. Let navigationType be "form-submission" if entryList is non-null; otherwise, "other".

Modify the plan to navigate algorithm to take an additional optional argument entryList (default null). Then, modify the step which calls navigate to pass it along:
  1. Navigate target browsing context to destination, with historyHandling set to historyHandling and navigationType set to "form-submission" entryList set to entryList .

Modify the submit as entity body algorithm to pass entry list along to plan to navigate as a second argument.

4.2. Browser UI/user-initiated patches

To more rigorously specify when a navigation is initiated from browser UI or by the user interacting with a, area, and form elements, both for the purposes of the NavigateEvent's userInitiated property and for prohibiting interception of certain types of browser-UI-initiated navigations, we need the following modifications:

Introduce (right before the definition of the navigate algorithm) the concept of a user navigation involvement, which is one of the following:

"browser UI"

The navigation was initiated by the user via browser UI mechanisms

"activation"

The navigation was initiated by the user via the activation behavior of an element

"none"

The navigation was not initiated by the user

Define the user navigation involvement for an Event event as "activation" if event’s isTrusted attribute is initialized to true, and "none" otherwise.

Modify the navigate algorithm to take an optional named argument userInvolvement (default "none"). Then, update the paragraph talking about browser-UI initiated navigation as follows:

A user agent may provide various ways for the user to explicitly cause a browsing context to navigate, in addition to those defined in this specification. Such cases must set the userInvolvement argument to "browser UI".

This infrastructure partially solves whatwg/html#5381, and it’d be ideal to update the `Sec-Fetch-Site` spec at the same time.

Modify the navigate to a fragment algorithm to take a new userInvolvement argument. Then, update the call to it from navigate to set userInvolvement to this userInvolvement value.

Modify the traverse the history by a delta algorithm to take an optional named argument userInvolvement (default "none"). Pass it along when calling apply the history step, which also needs to be modified to take such a argument. Then, update the paragraph talking about user-initiated navigation as follows:

When the user navigates through a browsing context, e.g. using a browser’s back and forward buttons, the user agent must traverse the history by a delta with the top-level traversable being operated on, a delta equivalent to the action specified by the user , and userInvolvement set to "browser UI" .

Modify the follow the hyperlink algorithm to take a new userInvolvement argument. Then, update the call to it from navigate to set userInvolvement to this userInvolvement value.

Modify the activation behavior of area elements by introducing the event argument and replacing the follow the hyperlink step with the following:
  1. Otherwise, follow the hyperlink created by element with the user navigation involvement for event.

Modify the activation behavior of a elements by replacing its follow the hyperlink step with the following:
  1. Otherwise, follow the hyperlink created by element with the user navigation involvement for event.

Expand the section on "Providing users with a means to follow hyperlinks created using the link element" by adding the following sentence:

Such invocations of follow the hyperlink algorithm must set the userInvolvement argument to "browser UI".

Modify the plan to navigate algorithm to take a userInvolvement argument. Then, update the call to it from navigate to set userInvolvement to this userInvolvement value.

Modify the submit algorithm to take an optional userInvolvement argument (default "none"). Have the submit algorithm pass along its value to all invocations of plan to navigate.

Modify the definition of the activation behavior for input elements to take an event argument. Then, pass along this argument to the invocation of the input activation behavior.

Modify the Submit Button state’s input activation behavior by having it take an event argument and pass along the user navigation involvement for event as the final argument when it calls submit.

Modify the Image Button state’s input activation behavior by having it take an event argument and pass along the user navigation involvement for event as the final argument when it calls submit.

Modify the button element’s activation behavior by having it take an event argument and, in the Submit Button case, to pass along the user navigation involvement for event as the final argument when it calls submit.

Modify the no-submit button case for implicit form submission to pass along "activation" as the final argument when it calls submit.

The case of implicit submission when a submit button is present is automatically taken care of because it fires a (trusted) click event at the submit button.

With the above infrastructure in place, we can actually fire and handle the navigate event in the following locations:

Modify the shared history push/replace state steps by inserting the following steps right before the step that runs the URL and history update steps.
  1. Let navigation be history’s relevant global object's navigation API.

  2. Let continue be the result of firing a non-traversal navigate event at navigation with navigationType set to historyHandling, isSameDocument set to true, destinationURL set to newURL, and classicHistoryAPISerializedData set to serializedData.

  3. If continue is false, then return.

Modify the navigate to a fragment algorithm by prepending the following steps. Recall that per § 4.2 Browser UI/user-initiated patches we have introduced a userInvolvement argument.
  1. Let navigation be navigable’s active window's navigation API.

  2. Let continue be the result of firing a non-traversal navigate event at navigation given with navigationType set to historyHandling, isSameDocument set to true, userInvolvement set to userInvolvement, and destinationURL set to url.

  3. If continue is false, return.

Expand the section of the navigation/traversal response handling which deals with 204s, 205s, and Content-Disposition: attachment responses with the following note:

These kinds of failed navigations or traversals will not be signaled to the navigation API (e.g., through the promises of any ongoing navigation, or the navigation.transition.finished promise, or the navigateerror event). Doing so would leak information about the timing of responses from other origins, in the cross-origin case, and providing different results in the cross-origin versus same-origin cases was deemed too confusing.

However, implementations could use this opportunity to clear any promise handlers for the navigation.transition.finished promise, as they are guaranteed at this point to never run. And, they might wish to report a warning to the console if any part of the navigation API initiated these navigations, to make it clear to the web developer the reason why their promises will never settle and events will never fire.

To convert a history handling behavior to a navigation type given a history handling behavior historyHandling:
  1. Assert: historyHandling is not "entry update".

  2. Return the result of switching on historyHandling:

    "reload"

    "reload"

    "replace"

    "replace"

    "default"

    "push"

Modify the apply the history step algorithm as follows. Near the end of the algorithm, inside the queued task over each global (currently step 14.10), before potentially unloading or activating the history entry, add the following step:
  1. Fire a traversal navigate event at displayedDocument’s relevant global object's navigation API with destinationEntry set to targetEntry and userInvolvement set to userInvolvement.

(Recall that we introduced the userInvolvement parameter as part of § 4.2 Browser UI/user-initiated patches.)

The current specification for downloading a hyperlink has several known issues, most notably whatwg/html#5548 which indicates that the specification should probably be merged into the general navigation algorithm.

For the purposes of the navigation API, we need to fire the appropriate navigate event, with downloadRequest set to the correct value. We could rigorously detail the ways to modify the current spec to accomplish this. But, given that the current spec will be rewritten anyway, this is probably not very useful. So until such a time as we can properly investigate and rewrite the downloading a hyperlink algorithm, we describe here the expected behavior in a less-formal fashion. We believe this is still enough to get interoperability.

5. Patches to session history

This section details monkeypatches to [HTML] to track appropriate data for associating a Navigation with a session history entry.

5.1. New session history entry items

Each session history entry gains the following new items:

5.2. Carrying over the navigation API key

Update the update the session history with the new page algorithm’s "replace" case by adding the following step after the construction of newEntry:
  1. If newEntry’s origin is the same as sessionHistory’s current entry's origin, then set newEntry’s navigation API key to sessionHistory’s current entry's navigation API key.

5.3. Carrying over the navigation API state

Update the navigate to a fragment algorithm by updating the step which creates a new session history entry to carry over the navigation API state from the active session history entry as well.

5.4. Tracking the origin member

Update the update the session history with the new page algorithm’s "replace" and "default" cases to set newEntry’s origin to newDocument’s origin as part of its creation.

Update the navigate to a fragment algorithm to set the new session history entry's origin to the current entry's document's origin.

Update the URL and history update steps algorithm to set the new session history entry's origin to document’s origin.

Potentially update the traverse the history algorithm to consult the new origin field, instead of checking the document's origin, since the document can disappear?? Needs further investigation.

5.5. Tracking the policy container member more often

The policy container field of the session history entry is null, except when we require storing the policy container in history. The specification for NavigationHistoryEntry.url requires the relevant referrer policy to be accessible for all entries, not just some.

So, update the HTML Standard to always store the policy container on the session history entry, and then only use it in the determine navigation params policy container algorithm in the cases currently covered by the require storing the policy container in history algorithm.

5.6. Updating the Navigation object

Update the update document for history step application algorithm by adding the following step nested inside the documentsEntryChanged check, after restoring the history object state but before firing popstate:
  1. Update the entries of document’s relevant global object's navigation API given "traverse".

    We can always pass "traverse" here because this call will result in currententrychange firing only for traversals; new document creation will not fire currententrychange, and same-document non-traversal navigations will have already updated navigation.currentEntry.

    This means any currententrychange event and dispose events will fire before the popstate and hashchange events.


Update the reactivate algorithm by adding the following step before the final one which checks the current document readiness:
  1. Update the entries of document’s relevant global object's navigation API given null and true.

    Passing true means that any dispose events will fire after the pageshow event, but this step being before the one below means that during the pageshow event navigation.entries() and navigation.currentEntry will have correctly-updated values. This split is motivated by a desire to have pageshow be the first event a page receives upon reactivation.


Update the URL and history update steps by appending the following step right after setting the active session history entry:
  1. Update the entries of document’s relevant global object's navigation API given historyHandling.


Update navigate to a fragment by appending the following step right after setting the active session history entry:
  1. Update the entries of navigable’s active window's navigation API given historyHandling.


We do not update the entries when initially creating a new browsing context, as we intentionally don’t want to include the initial about:blank Document in any navigation history entry list.

6. Other patches

6.1. Focus tracking

To support the focusReset option, the following patches need to be made:

Update the focusing steps to, right before they call the focus update steps, set the Document's relevant global object's navigation API's focus changed during ongoing navigation to true.

Update the focus fixup rule to additionally set the Document's relevant global object's navigation API's focus changed during ongoing navigation to false.

In combination, these ensure that the focus changed during ongoing navigation reflects any developer- or user-initiated focus changes, unless they were undone by the focus fixup rule. For example, if the user moved focus to an element which was removed from the DOM while the promise returned from a handler passed to intercept() was settling, then that would not count as a focus change.

6.2. Scroll restoration

To support the scrollRestoration option, as well as to fix whatwg/html#7517, the following patches need to be made:

Add a boolean, has been scrolled by the user, initially false, to Document objects. State that if the user scrolls the document, the user agent must set that document’s has been scrolled by the user to true. Modify the unload a document algorithm to set this back to false.

Define the process of restoring scroll position data given a session history entry entry as follows:
  1. Let document be entry’s document.

  2. If document’s has been scrolled by the user is true, then the user agent should return.

  3. The user agent should attempt to use entry’s scroll position data to restore the scroll positions of document’s restorable scrollable regions. The user agent may continue to attempt to do so periodically, until document’s has been scrolled by the user becomes true.

    This is formulated as an attempt, which is potentially repeated until success or until the user scrolls, due to the fact that relevant content indicated by the scroll position data might take some time to load from the network.

    Scroll restoration might be affected by scroll anchoring. [CSS-SCROLL-ANCHORING-1]

With this in place, modify the restore persisted state algorithm’s first step to read as follows:
  1. If entry’s scroll restoration mode is "auto", and entry’s document's relevant global object's navigation API's suppress normal scroll restoration during ongoing navigation is false, then restore scroll position data given entry.

In addition to the existing note, add the following one:

If the suppress normal scroll restoration during ongoing navigation boolean is true, then restoring scroll position data might still happen at a later point, as part of potentially performing scroll restoration for the relevant Navigation object, or via a navigateEvent.restoreScroll() method call.

6.3. Canceling navigation and traversals

The existing HTML specification discusses canceling a navigation and traverals in a few places. However, the process is not very well-defined, and per whatwg/html#6927, is not very interoperable. We plan to make it more rigorous, after the session history rewrite lands.

Specifically, the spec uses a few phrases:

whatwg/html#6927 reveals that implementations don’t really follow this breakdown. In particular, modulo one case in Firefox, traversals are only canceled as part of discarding a browsing context.

That leaves us with two main operations: canceling not-yet-mature navigations, and dealing with browsing context discarding.


The navigation API introduces a new complication here, which is that a navigation might have matured but still be "ongoing", in the sense of § 1.3 Ongoing navigation tracking. That is, consider a case such as:

navigation.addEventListener("navigate", e => {
  e.intercept({
    handler() {
      return new Promise(r => setTimeout(r, 1_000));
    }
  });
  e.signal.addEventListener("abort", () => { ... });
});

const p = navigation.navigate("#1");

setTimeout(() => window.stop(), 500);

Without the navigate event handler, this kind of synchronous fragment navigation would be straightforward: it matures synchronously, and the stop() call does nothing. But because we have used the navigate handler to indicate that the navigation is still ongoing, we want the stop() call to finalize that navigation with an aborted navigation error, in particular causing p to reject and the abort event to fire on e.signal.


The integration is then as follows:

7. Security and privacy considerations

7.1. Cross-site tracking

This specification does not enable any new cross-site tracking capabilities. This is largely because the navigation.entries() method only returns information about the same-origin, same-frame history entries.

In more detail:

7.2. Navigation monitoring and interception

Through the navigate event, this API allows web developers to monitor navigations, and in some cases replace cross-document navigations with same-document ones, or prevent the navigation from going through.

Care has been taken to avoid this being dangerous, or giving insight into user behavior that would not otherwise be available to the site. In particular:

We also have a few more restrictions worth noting, which don’t directly address any attack, but reduce the surface area and complexity of the navigation interception feature, which can have indirect security benefits:

7.3. URL updates

This API, like history.pushState() and history.replaceState(), gives the ability to change what is shown in the browser’s URL bar. This is part of the navigation interception capability mentioned in the previous section.

This is not dangerous, because the navigation API is subject to the same restrictions as the classic history API: namely, the page’s URL can only be changed if the page can have its URL rewritten to the new URL. So in particular no authority-granting components, such as the site or origin, are impacted.

7.4. Other user agent UI

This specification does not add any requirements on how user agents implement their user interfaces. (Even the URL bar updates mentioned in the previous section are not technically part of the specification; the specification only governs the return value of other APIs, like location.href.) This preserves the ability for user agents to protect users through UI changes.

For example, today some user agents take advantage of this flexibility to skip certain history entries when pressing the back button. This can be used to avoid back-trapping by abusive sites, by skipping entries with which the user did not interact and thus allowing the user to escape abusive sites faster.

The navigation API preserves all of these freedoms.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CONSOLE]
Dominic Farolino; Robert Kowalski; Terin Stock. Console Standard. Living Standard. URL: https://console.spec.whatwg.org/
[CSS21]
Bert Bos; et al. Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification. URL: https://drafts.csswg.org/css2/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HR-TIME-3]
Yoav Weiss. High Resolution Time. URL: https://w3c.github.io/hr-time/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[REFERRER-POLICY]
Jochen Eisinger; Emily Stark. Referrer Policy. URL: https://w3c.github.io/webappsec-referrer-policy/
[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/
[XHR]
Anne van Kesteren. XMLHttpRequest Standard. Living Standard. URL: https://xhr.spec.whatwg.org/

Informative References

[CSP]
Mike West. Content Security Policy Level 3. URL: https://w3c.github.io/webappsec-csp/
[CSS-SCROLL-ANCHORING-1]
Tab Atkins Jr.. CSS Scroll Anchoring Module Level 1. URL: https://drafts.csswg.org/css-scroll-anchoring/
[WEBAPPSEC-FETCH-METADATA-1]
Fetch Metadata Request Headers URL: https://w3c.github.io/webappsec-fetch-metadata/

IDL Index

partial interface Window {
  [Replaceable] readonly attribute Navigation navigation;
};

[Exposed=Window]
interface Navigation : EventTarget {
  sequence<NavigationHistoryEntry> entries();
  readonly attribute NavigationHistoryEntry? currentEntry;
  undefined updateCurrentEntry(NavigationUpdateCurrentEntryOptions options);
  readonly attribute NavigationTransition? transition;

  readonly attribute boolean canGoBack;
  readonly attribute boolean canGoForward;

  NavigationResult navigate(USVString url, optional NavigationNavigateOptions options = {});
  NavigationResult reload(optional NavigationReloadOptions options = {});

  NavigationResult traverseTo(DOMString key, optional NavigationOptions options = {});
  NavigationResult back(optional NavigationOptions options = {});
  NavigationResult forward(optional NavigationOptions options = {});

  attribute EventHandler onnavigate;
  attribute EventHandler onnavigatesuccess;
  attribute EventHandler onnavigateerror;
  attribute EventHandler oncurrententrychange;
};

dictionary NavigationUpdateCurrentEntryOptions {
  required any state;
};

dictionary NavigationOptions {
  any info;
};

dictionary NavigationNavigateOptions : NavigationOptions {
  any state;
  NavigationHistoryBehavior history = "auto";
};

dictionary NavigationReloadOptions : NavigationOptions {
  any state;
};

dictionary NavigationResult {
  Promise<NavigationHistoryEntry> committed;
  Promise<NavigationHistoryEntry> finished;
};

enum NavigationHistoryBehavior {
  "auto",
  "push",
  "replace"
};

[Exposed=Window]
interface NavigationCurrentEntryChangeEvent : Event {
  constructor(DOMString type, NavigationCurrentEntryChangeEventInit eventInit);

  readonly attribute NavigationType? navigationType;
  readonly attribute NavigationHistoryEntry from;
};

dictionary NavigationCurrentEntryChangeEventInit : EventInit {
  NavigationType? navigationType = null;
  required NavigationHistoryEntry destination;
};

[Exposed=Window]
interface NavigationTransition {
  readonly attribute NavigationType navigationType;
  readonly attribute NavigationHistoryEntry from;
  readonly attribute Promise<undefined> finished;
};

[Exposed=Window]
interface NavigateEvent : Event {
  constructor(DOMString type, NavigateEventInit eventInit);

  readonly attribute NavigationType navigationType;
  readonly attribute NavigationDestination destination;
  readonly attribute boolean canIntercept;
  readonly attribute boolean userInitiated;
  readonly attribute boolean hashChange;
  readonly attribute AbortSignal signal;
  readonly attribute FormData? formData;
  readonly attribute DOMString? downloadRequest;
  readonly attribute any info;

  undefined intercept(optional NavigationInterceptOptions options = {});
  undefined restoreScroll();
};

dictionary NavigateEventInit : EventInit {
  NavigationType navigationType = "push";
  required NavigationDestination destination;
  boolean canIntercept = false;
  boolean userInitiated = false;
  boolean hashChange = false;
  required AbortSignal signal;
  FormData? formData = null;
  DOMString? downloadRequest = null;
  any info;
};

dictionary NavigationInterceptOptions {
  NavigationInterceptHandler handler;
  NavigationFocusReset focusReset;
  NavigationScrollRestoration scrollRestoration;
};

enum NavigationFocusReset {
  "after-transition",
  "manual"
};

enum NavigationScrollRestoration {
  "after-transition",
  "manual"
};

callback NavigationInterceptHandler = Promise<undefined> ();

enum NavigationType {
  "reload",
  "push",
  "replace",
  "traverse"
};

[Exposed=Window]
interface NavigationDestination {
  readonly attribute USVString url;
  readonly attribute DOMString? key;
  readonly attribute DOMString? id;
  readonly attribute long long index;
  readonly attribute boolean sameDocument;

  any getState();
};

[Exposed=Window]
interface NavigationHistoryEntry : EventTarget {
  readonly attribute USVString? url;
  readonly attribute DOMString key;
  readonly attribute DOMString id;
  readonly attribute long long index;
  readonly attribute boolean sameDocument;

  any getState();

  attribute EventHandler ondispose;
};