CSS & layout

Route and navigation matching, proposed for CSS

Route and navigation matching, proposed for CSS

You want a slide-left when the user goes from home to about, and a slide-right on the way back. You have multi-page View Transitions in your corner. So you write a pagereveal listener, read location, match URL patterns, set types in script, and remember to set the outbound side in pageswap. Behaviour lives in JS. The stylesheet only gets to react to whatever names you managed to pin on the DOM in time.

Bramus's write-up on the new CSS Route and Navigation Matching proposal (draft name css-navigation-1, co-authored with Noam Rosenthal and David Baron) flips that. URLs become named things the stylesheet already knows about, and the transition rules read like any other conditional CSS.

Naming routes with @route

@route gives a URL pattern a name. The name is a custom identifier (double-dash prefixed) that other rules can point at:

@route --home { pathname: url-pattern('/'); }
@route --about { pathname: url-pattern('/about'); }
@route --detail { pathname: url-pattern('/detail/:id'); }

The matching engine inside url-pattern() is path-to-regexp, the same syntax the JavaScript URLPattern API already uses, so /detail/:id means what you'd expect. The proposal lists protocol, hostname, port, pathname, search and hash as descriptors, so a single @route can constrain more than the path.

Querying the current move

@navigation is where you ask "which navigation is happening right now?" using from: and to: conditions against your named routes:

@navigation (from: --home) and (to: --about) {
  @view-transition {
    navigation: auto;
    types: slide-all-to-left;
  }
}

@navigation (from: --about) and (to: --home) {
  @view-transition {
    navigation: auto;
    types: slide-all-to-right;
  }
}

Read that as a CSS-native replacement for the pageswap/pagereveal branch. No JS listener, no manual location.pathname matching, no imperative call to set the transition types. The types are declared next to the routes that trigger them.

Two more keywords widen the net. between matches both directions of a pair, and at narrows to one side of that pair (the outgoing side or the incoming side), so you can bracket an entire round trip in one block:

@navigation (between: --home --detail) {
  @navigation (at: --home) {
    :nav-source img {
      view-transition-name: image;
    }
  }
  @navigation (at: --detail) {
    img#hero {
      view-transition-name: image;
    }
  }
}

Two selectors that finally see the navigation

The block above leans on :nav-source, a pseudo-class that matches the element which initiated the outgoing navigation. It's modelled on NavigateEvent.sourceElement. That's the piece the declarative approach has been missing: the clicked thumbnail can pick up its own view-transition-name at the exact moment the transition is being captured, without a script traversing the DOM to hand-tag it first.

The companion is :link-to(), a functional selector that matches links by where they'd go:

:link-to(--detail) {
  color: hotpink;
}

A route name goes in, and every <a> pointing at a URL that matches that route gets styled. Bramus flags that parameter-scoped matching (only --detail routes where id equals 6, for example) is still being worked on.

Where this sits

None of the above ships in a browser yet. It's a draft spec on the way to the CSS Working Group F2F meeting in Berlin the week after the post, following the initial CSSWG introduction in January and a developer feedback session at CSS Day in June. The interesting bit for anyone building multi-page apps is the shape it commits to: URLs become part of the same declarative surface as media queries, container queries and @view-transition. The pageswap/pagereveal handlers stay in the platform, but the common cases stop needing them.

Two things worth trying in your head before you agree with the direction. First, custom-ident routes are cheap to add (--home, --detail, --auth) but they're global, and route-name collisions across a large codebase will feel like z-index bugs unless the working group nails a scoping story. Second, the moment :link-to() lands, "style outbound links to the checkout differently" stops being a data-attribute exercise. That's a small win with a lot of surface area.

Source: Bram.us (bram.us)

Related
CSS & layout

Why setting perspective on :root won't flip your view transition

Setting perspective on html, :root, or even ::view-transition itself looks like the obvious place to enable a 3D view transition — and produces nothing. The fix is to switch from the property form to the perspective() function, applied inside the snapshot's keyframes.

June 24, 2026
CSS & layout

Naming many view transitions: attr() or match-element?

Two modern ways to give every card in a grid its own view-transition-name — attr() reading a data attribute, or the match-element keyword. They look interchangeable, then diverge the moment a navigation crosses documents or a pseudo-selector needs to address a specific snapshot.

June 24, 2026
CSS & layout

Hard color stops turn conic-gradient() into a triangle machine

Chris Coyier makes the case that conic-gradient() with two hard color stops is the fastest way to draw a triangle in CSS. The from and at keywords do most of the work.

July 29, 2026