Skip to content

Commit 4bef93f

Browse files
authored
Revise route-matching explainer for clarity and updates
Updated the document to reflect the current state of the work, including changes to the related links and enhancements for CSS navigation.
1 parent 9e62b1f commit 4bef93f

1 file changed

Lines changed: 190 additions & 57 deletions

File tree

route-matching-explainer.md

Lines changed: 190 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
# Declarative Route Matching
22

3-
[@noamr](https://github.com/noamr) and [@dbaron](https://github.com/dbaron), September 2025 / December 2025
4-
5-
(This document is currently partway through being updated to reflect the current state of the work.)
3+
[@noamr](https://github.com/noamr) and [@dbaron](https://github.com/dbaron)
64

75
# Related links
86

97
* [current css-navigation-1 specification draft](https://drafts.csswg.org/css-navigation-1/)
108
* [current specification issues (open and closed)](https://github.com/w3c/csswg-drafts/issues?q=label%3Acss-navigation-1%20is%3Aissue)
119
* initial syntax discussion for HTML route matching: https://github.com/WICG/declarative-partial-updates/issues/46
12-
* initial discussion for CSS route matching: https://github.com/w3c/csswg-drafts/issues/12594
10+
* Discussion for CSS route matching: https://github.com/w3c/csswg-drafts/issues/12594
1311

1412
# Motivation and Use Cases
1513

@@ -38,6 +36,7 @@ This means adding features that:
3836
* match patterns of URLs by exposing [URL Patterns](https://urlpattern.spec.whatwg.org/) in CSS
3937
* apply styles conditionally based on the origin and destination of the current navigation, so that transitions between particular sets of URLs (whether separate documents or separate states/routes within a single page app) can be styled
4038
* style an HTML link based on its target matching both the origin or destination of the current navigation *and* matching a particular URL pattern, which allows matching the correct item within the list in a list-to-details or details-to-list transition.
39+
* apply styles based on the history type (e.g. back/forward) and phase (e.g. loading/committed) of the navigaiton
4140

4241
## Navigation-aware styling
4342
### Framework routers
@@ -74,13 +73,10 @@ function Navbar() {
7473
What if it could look like this?
7574
```html
7675
<style>
77-
@route (to: home) {
78-
a:remote-link + spinner { opacity: 100%; }
79-
}
80-
81-
@route (to: about) {
82-
a:remote-link { color: grey }
83-
}
76+
@route --home { pathname: "/"; base-url: document; }
77+
@route --about { pathname: "/about"; base-url: document; }
78+
:active-navigation(to --home) + spinner { opacity: 100%; }
79+
:active-navigation(to --about) { color: grey }
8480
</style>
8581
<nav>
8682
<a href="/">Home</a>
@@ -106,20 +102,7 @@ navigation.addEventListener("navigate", e => {
106102
107103
## Two-phase preview view transitions
108104
109-
See https://github.com/w3c/csswg-drafts/issues/12829
110-
111-
When performing a cross-document view-transition, the transition often has to delay until the next document is ready.
112-
By using route-matching, we can render a "preview" of the new state using style only, and transition to that instantly, before continuing to the final content.
113-
114-
```css
115-
@route (to: article) {
116-
.article-skeleton { display: block }
117-
}
118-
119-
@navigation {
120-
view-transition: with-preview;
121-
}
122-
```
105+
See [two-phase view transitions explainer](https://github.com/w3c/csswg-drafts/blob/main/css-view-transitions-2/two-phase-transition-explainer.md#solution-2-declarative-preview-view-transitions--navigation-preview-state)
123106
124107
## Declarative same-document view transitions
125108
@@ -146,13 +129,193 @@ navigation.addEventListener("navigate", e => {
146129
});
147130
```
148131
149-
## Style based on current route
132+
<h1>CSS Navigations 1</h1>
133+
134+
135+
The scope of `css-navigation-1` is to:
136+
* change style conditionally (importantly including view transition) based on current navigation state
137+
* apply style to links that participate in navigations
138+
139+
<h2>URL patterns</h2>
140+
141+
One main issue with URL matching is that URL comparison is notoriously finicky.
142+
For example, a link to the `/about` page might be written as `/about`, `/about/`, or even `/about?utm_source=something`.
143+
144+
This has been addressed by the [URL Pattern API](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API), by allowing
145+
a `URLPattern` to act as a "matcher" - a set of rules to extract parameters from a URL or match it against another URL.
146+
147+
The `css-navigation-1` happily adopts the concept of URL patterns.
148+
149+
<h2>Routes</h2>
150+
151+
The `css-navigation-1` spec exposes a `@route` rule, which allows naming a URL or url pattern for use by conditional `@navigation` rules or link styling.
152+
```css
153+
@route --home {
154+
pathname: "/";
155+
base-url: document;
156+
}
157+
```
158+
159+
A `@route` can be a URL pattern or a URL.
160+
161+
In addition, the `url-pattern(string)` function can be used to define a quick URL pattern without having to name it.
162+
163+
<h2>Active navigation state</h2>
164+
165+
Both link matching and conditional navigation styling rely on the concept of "active navigation state".
166+
The active navigation state is defined in terms of the HTML standard, and contains:
167+
* The old & new URLs
168+
* The old & new history index
169+
* type (push, replace, traverse, reload)
170+
* phase (loading, ready, committed)
171+
* The source element (clicked link, form, or submit button)
172+
173+
The CSS exposed definitions all rely on this state and reflect it.
174+
175+
This state is processed differently based on one of the following scenarios:
176+
* For a same-document navigation, it relies on the navigation API and on its definition of whether a navigation is "committed".
177+
* For a cross-document navigation, it starts when the navigation is initiated, and ends in the first frame of the new page.
178+
179+
When same-document navigations occur before the new page is rendered, the cross-document navigation takes precedence,
180+
as the same-document navigation is "non-visual" so cannot be styled.
181+
182+
183+
<h2>Conditional navigation styling</h2>
184+
185+
The `@navigation` at rule, as well as its corresponding `if (navigation())` clause, matches the rule with the above active navigation state.
186+
187+
188+
### URL matching
189+
190+
```css
191+
@navigation (from: --home) { ... }
192+
@navigation (to: --about) { ... }
193+
@navigation (between: --home and --about) { ... }
194+
@navigation (with: url-pattern("/*")) { ... }
195+
@navigation (at: url("/exact/?a=b")) { ... }
196+
```
197+
198+
The `from`, `to`, `with`, and `at` queries define which URL to match in the active navigation state.
199+
The `from` and `to` URLs are always the old & new ones, while `with` and `at` start as being equivalent to `from` and `to`, and swap when the navigation is committed, either by a same-document navigation API commit, or by the pages swapping in a cross-document navigation.
200+
201+
202+
### Phase matching
203+
204+
```css
205+
@navigation (phase: loading) { ... }
206+
@navigation (phase: ready) { ... }
207+
@navigation (phase: committed) { ... }
208+
```
209+
210+
The `phase` query represents the phase of the active navigation state.
211+
The `ready` phase is a bit special, and is only active in a (same-origin) cross-document navigation when the new document is ready but not swapped yet, e.g. for the purpose of showing a [preview](https://github.com/w3c/csswg-drafts/blob/main/css-view-transitions-2/two-phase-transition-explainer.md#solution-2-declarative-preview-view-transitions--navigation-preview-state) or for capturing the old state of a cross-document view transition.
212+
213+
214+
<h3>Navigation type matching</h3>
215+
216+
```css
217+
@navigation (history: navigate) { ... }
218+
@navigation (history: reload) { ... }
219+
@navigation (history: traverse) { ... }
220+
@navigation (history: back) { ... }
221+
@navigation (history: forward) { ... }
222+
```
223+
224+
The `history` query matches with the active navigation state's navigation type and session history indices.
225+
It can help style different view transitions (or any other navigation-based style) differently based on the type of navigation.
226+
e.g.:
227+
228+
```css
229+
@view-transition {
230+
navigation: auto;
231+
types: slide-from-right;
232+
}
233+
234+
@navigation (history: back) {
235+
@view-transition {
236+
navigation: auto;
237+
types: slide-from-left;
238+
}
239+
}
240+
```
241+
242+
## Link matching
243+
244+
Apart from conditionally styling based on a navigation, links can be styled if they participate in a navigation.
245+
246+
### Link matching by route
247+
248+
The `:link-to` pseudo-class doesn't take navigation into account, but allows for matching a link URL without resorting to string matching of the `href` attribute:
249+
250+
```css
251+
a:link-to(--home) { ... }
252+
a:link-to(url-pattern("/about")) { ... }
253+
a:link-to(url("/exact?a=1")) { ... }
254+
```
255+
256+
### Matching the navigation's source element
257+
258+
The navigation's [source element](https://html.spec.whatwg.org/multipage/#navigation-source-element) is a link, form, or submit button.
259+
260+
The `:trigger-link` pseudo class allows styling that particular link, for the course of the navigation:
261+
262+
```css
263+
:trigger-link { animation-name: blink; }
264+
```
265+
266+
Note that `:trigger-link` only matches link elements (similar to `:any-link`).
267+
268+
### Matching a link that matches the active navigation
269+
270+
While `:trigger-link` is useful for matching the actual link that was clicked, for some use cases it is not sufficient.
271+
For example, when going from movie details to a movie list full of thumbnails, the particular thumbnail of the movie should be style,
272+
however it was not clicked!
273+
274+
The `:active-navigation` pseudo-class uses route-matching, similar to `@navigation`, to match a link with an active navigation:
275+
276+
```css
277+
@route --movie-list {
278+
pathname: "/*/movies";
279+
base-url: document;
280+
}
281+
282+
@route --movie-details {
283+
pathname: "/*/movie/:id";
284+
base-url: document;
285+
}
286+
287+
@navigation ((at: --movie-details) and (with: --movie-list)) {
288+
.hero { view-transition-name: hero-or-thumb }
289+
}
290+
291+
@navigation (at: --movie-list) {
292+
a:active-navigation(with --movie-details) {
293+
.thumb { view-transition-name: hero-or-thumb }
294+
}
295+
}
296+
```
297+
298+
When matching a link with an active navigation, by default the link's `href` URL is compared against the navigation URL.
299+
However, when a URL pattern route is given like in the above example, each of these links are processed by the given URL pattern
300+
and the resulting named groups are matched. This allows comparing a navigation URL with a link in a "lossy" way that allows ignoring
301+
some URL parameters while respecting others.
302+
303+
To illustrate, the above `:active-navigation` rule would match the following:
304+
* a link to `/en/movie/123` when navigating to or from `/en/movie/123`
305+
* a link to `/en/movie/123` when navigating to or from `/fr/movie/123`
306+
* a link to `/en/movie/123?from=list` when navigating to or from `/es/movie/123#scroll-here`
307+
308+
This might seem confusing at first but this "non-exact" match of URLs is essential given how URLs can carry multiple bits of information.
309+
310+
# Potential future enhancements
311+
312+
## Future enhancement: Style based on current route
150313
In addition to the curation of the navigation itself, it is a common technique in modern web apps to have a "shell" that is common between pages and mostly static, and an "outlet" area for the dynamic content.
151314
However, some parts of the shell often still have some dynamic parts that appear on "some" pages or in some scenarios, or appear different based on the current page.
152315
153316
For example, a chat widget or members area might only appear in certain pages. A "related" `<aside>` element might only appear in article pages.
154317
155-
# The initial proposed solution: HTML route map with CSS reflection
318+
## Future enhancement: HTML route map with CSS reflection
156319
- Routes are declared in HTML, to avoid requiring all the stylesheets to know the different route URLs or leak those URLs directly, and also to allow future enhancements that are not necessarily style-based.
157320
- A route at is core is a named `URLPattern`.
158321
- Matching a route can be toggle-like event target, similar to media-query matching. It can help responding to specific route changes without having to intercept *all* navigations.
@@ -181,36 +344,6 @@ For example, a chat widget or members area might only appear in certain pages. A
181344
</body>
182345
```
183346
184-
## CSS reflection
185-
186-
Naming a set of these rules in HTML already gives us something that CSS can build on:
187-
```css
188-
@route (home) {
189-
#chat-widget { display: none; }
190-
}
191-
192-
nav {
193-
a:remote-link(pending) .spinner {
194-
animation: spin;
195-
}
196-
}
197-
198-
/* navigation-based view-transition can work out of the box
199-
because we can count on the final CSS state */
200-
@view-transition {
201-
navigation: auto;
202-
}
203-
204-
/* or be route-specific */
205-
@route (to: article) {
206-
@view-transition {
207-
navigation: auto;
208-
types: slide-3d;
209-
}
210-
}
211-
```
212-
213-
# Potential future enhancements
214347
## Declarative interception & history-handling
215348
216349
In addition to CSS reflection, some basic navigation interception capabilities can be provided out of the box:

0 commit comments

Comments
 (0)