Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ backButtonEl.addEventListener("click", () => {
- [Notifications on entry disposal](#notifications-on-entry-disposal)
- [Current entry change monitoring](#current-entry-change-monitoring)
- [Complete event sequence](#complete-event-sequence)
- [Extensions to the Anchor element](#extensions-to-the-anchor-element)
- [Default navigation type](#default-navigation-type)
- [The navigateinfo="..." and navigatestate="..." attributes](#the-navigateinfo-and-navigatestate-attributes)
- [Additional example](#additional-example)
- [Guide for migrating from the existing history API](#guide-for-migrating-from-the-existing-history-api)
- [Performing navigations](#performing-navigations)
- [Warning: back/forward are not always opposites](#warning-backforward-are-not-always-opposites)
Expand Down Expand Up @@ -1033,6 +1037,92 @@ The commit steps are:
1. Any now-unreachable `NavigationHistoryEntry` instances fire `dispose`.
1. The URL bar updates.

### Extensions to the Anchor element
Currently an `<a>` only supports push navigations. This proposal aims to extend its existing behaviour with a few new and existing attributes.
Namely:
- `reload=""`, `replace=""` and `traverse=""` as boolean attributes.
- `navigateinfo=""` and `navigatestate=""` as string attributes.
- `href="..."` and `rel="next/prev"` with the addition of the string attribute `key="..."` as traversal hints.

#### Default navigation type
The default navigation type of a `<a>` is always a push navigation. The new boolean attributes `replace=""`, `reload=""` and `traverse=""` serve as the default navigation type modifier.

The default navigation type will still be a push navigation.
```html
<a href="posts">Posts</a>
```

The `reload=""` attribute will reload the current entry. Note: the `href=""` attribute is ignored.
```html
<a reload>Refresh results</a>
```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the benefits of this over <button onclick="location.reload()">? It's unclear if reloading really fits the hyperlink paradigm well...

Copy link
Contributor Author

@absurdprofit absurdprofit Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I'm not very sure either, I added it more for completeness sake. I did think it would be nice to have a "open in next tab" functionality for a refresh UI element which a <button> would lack, which is why I still added it. Beyond content security policies blocking inline JS I don't see any problem with just using JS like in your example to achieve the same behaviour. If there isn't much interest in something like this I would drop it.

Copy link
Contributor Author

@absurdprofit absurdprofit Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do have a question though, what kind of navigation does <a href='.'> trigger i.e. what should NavigateEvent.navigationType yield? I tried this in chrome a few times, sometimes it would be a push navigation and sometimes it would be a replace navigation. I ask because my initial intuition was this would have been a "reload" navigation but since that wasn't the case I felt motivated to add a keyword for it.


The `replace=""` attribute in addition to the `href="..."` attribute will replace the current entry with a new history entry.
```html
<a href="/login" replace>Logout</a>
```
The `traverse=""` attibute allows `<a>`s to link to existing entries. When using the `traverse=""` navigation type modifier the `rel="next/prev"`, `key="..."` and `href="..."` attributes, become optional hints.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar questions for the href-less versions here.

Additionally, to get key=""s you need to use JavaScript. So I can't really understand when you would use this.

I can see a use case for <a href="/" rel="prev" traverse>, however.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key="..." attribute is merely a convenience instead of a Super Declarative™ API. This exists as a method for when you want to be super precise about which entry you navigate to unlike the href="..." pattern matching feature. Additionally having an <a> tag that navigates to a specific existing entry without clickjacking is essentially impossible without something like the NavigateEvent.sourceElement feature.

A solid use case for something like this that I thought of is implementing breadcrumbs, most sites currently implement such a feature using the basic push navigation behaviour of the <a> which can lead to a messy history stack as a "previous" page now exists at the head of the history stack.

The `rel="..."` is an existing attribute that hints at the relationship of the linked URL to the current document. When used with `traverse=""` it serves as a directional hint.
```html
<a rel="prev" traverse>Go back</a>
<a rel="next" traverse>Go forward</a>
```
The `href="..."` is an existing attribute that specifies the URL the `<a>` points to. When used with the `traverse=""` attribute the anchor will point to the closest entry with a matching URL. The `rel="next/prev"` attribute can be used to specify the search direction.
```html
<a href=".." traverse>Tab 1</a> <!-- Given the current URL pathname is /parent/child this anchor navigates to an entry with /parent as the pathname. -->
<a href="/" rel="prev" traverse>Go home</a>
<a href="/posts" rel="next" traverse>Posts</a>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if there are no links in history with these hrefs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are no links in the history then it should fallback to the base navigate behaviour. The default base navigate behaviour for <a>s as they exist now is "push", meaning if a link doesn't exist it simply becomes a push navigation. The proposal also adds the ability to modify this "base navigate behaviour" so for example a replace="" boolean attribute would make the fallback instead be a replace navigation.

```
The `key="..."` is a new attribute that allows you to specify a history entry key the `<a>` points to. This attribute only works with the `traverse=""` attribute and is ignored otherwise. If no matching entry is found the behaviour is the same as `<a>Dead link</a>`.
```html
<a key="7c6a6481-c4bc-40f5-9617-0287441a3418" traverse>Go to 7c6a6481-c4bc-40f5-9617-0287441a3418</a>
```
The `key="..."` attribute takes precedence over `href=""` meaning if an entry with a matching key isn't found, the anchor will fallback to traversal with the `href="..."` hint.
```html
<a href="/" key="7c6a6481-c4bc-40f5-9617-0287441a3418" traverse>Go home</a>
```

The `traverse=""` boolean attribute will always take precedence over push, replace and reload navigation types. Meaning if no matching entry is found the `<a>` will fallback to the next navigation type.
```html
<a href="/" rel="prev" traverse replace>Go home</a> <!-- / Will replace the current entry if no matching entry is found -->
```

#### The `navigateinfo="..."` and `navigatestate="..."` attributes
Finally two string attributes have been added for passing state and info via an `<a>`.
```html
<a id="home" href="/" navigateinfo="push" navigatestate="newuser">Home</a>
```
The new attributes will have corresponding DOM properties for passing non-string data types to entries.
```js
const a = document.getElementById("home");
a.navigatestate = {
user: {
name: "John Doe",
},
};
a.navigateinfo = {
type: "push",
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this gets pretty messy.

The issues mostly come about with the interaction between the JS properties and the attributes.

  • What does the attribute return, when the JS property is set to an object?
  • What does the JS property return, when the attribute is set to a string?
  • How does el.setAttribute("navigatestate", el.getAttribute("navigatestate")) behave? Does it change what state is sent? Does it change the value of the JS property?
  • How does el.navigatestate = el.navigatestate behave? Does it change what state is sent? Does it change the value of the attribute?

I would suggest leaving these as strings only and saying that if you want more complex objects, you need to use JavaScript.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • The getAttribute() would return the attribute as seen in HTML.
  • The JS property at first would be the same as the HTML attribute. In other words the attribute would serve as a "default value".
  • No, the attribute wouldn't change the value of the JS property. The JS property takes precedent in the state that is sent.
  • No, the JS property wouldn't change the value of the attribute.

TLDR; the idea here is the navigatestate and navigateinfo properties/attributes would mirror the behaviour of the value attribute/property from the <input> element. The attribute has no effect on the property beyond its use as a "default value".

This wasn't very clear in the demo and I apologise.

```

Note: `navigatestate=""` is ignored for traverse navigations.
```html
<a href="/posts" navigateinfo="push" navigatestate="newstate">Home</a>
<a navigateinfo="reload" navigatestate="newstate" reload>Refresh</a>
<a href="/login" navigateinfo="replace" navigatestate="newstate" replace>Logout</a>
<a rel="prev" navigateinfo="traverse" navigatestate="newstate" traverse>Back</a> <!-- navigatestate="newstate" is ignored -->
```

#### Additional Example
Tabbed navigation could be implemented without JavaScript. By using `traverse=""` anchors will point to existing entries if they exist but fallback to a push navigation if a matching entry doesn't already exist.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually a user experience that people want? Are there any sites today that are implementing this kind of user experience using JavaScript?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main tabs in Gmail are implemented using JavaScript, however like many implementations online these navigations aren't added to the history stack and it's not trivial to implement synchronisation with the history stack especially with the old history API.
image

This is also the case for Azure which is an SPA that uses hash based navigation but they still don't persist these navigations to the stack.
Screenshot 2025-01-30 212946

I couldn't find any example of tab navigation by history stack traversal via JS but from my experience a lot of the anxiety around doing things like that on the web comes from the fact that it's hard. Also the default navigation behaviour of an <a> being push further encourages developers to use JavaScript based solutions that aren't synched to the browser history stack to avoid duplicate history entries polluting the stack (among other issues).

One problem with omitting tab navigation from the browser history is that users lose their position upon reloading the page, forcing them to manually restore their previous state. Additionally, tabs are not directly linkable, which can be frustrating if you wish to share or revisit a specific tab. To address this in the past, I have implemented a solution that synchronizes the tab state with the query parameters in the URL. This approach ensures that users can reload the page without losing their current tab selection while also making tabs linkable. At the same time, it avoids polluting the history stack with duplicate entries by updating the URL without pushing new history entries unnecessarily. By reading the initial tab state from window.location.search, I could restore the current tab when the page loads. CodeSandbox implements file tabs like this.

```html
<nav class="tabs">
<a class="tab" href="tab-1" traverse>Tab 1</a>
<a class="tab" href="tab-2" traverse>Tab 2</a>
<a class="tab" href="tab-3" traverse>Tab 3</a>
</nav>
```

## Guide for migrating from the existing history API

For web developers using the API, here's a guide to explain how you would replace usage of `window.history` with `window.navigation`.
Expand Down
Loading