View Transitions
Animated page changes and shared-element morphs via the browser's View Transitions API - one option to turn on, zero JavaScript to write.
One option, sensible defaults
With o.ViewTransitions = true, every navigation's render is wrapped in
document.startViewTransition. The built-in default animations
(ViewTransitionDefaultAnimations, on by default) are direction-aware: forward
navigations glide in, Back/Forward mirror the motion, replaces cross-fade - the router
detects direction from the navigation type (Push / Replace /
Pop) and exposes it as data-brouter-nav on the root element for
your own CSS. On browsers without the API everything is inert; nothing breaks. Only actual
navigations animate - the initial load never does, so with prerendering the interactive
takeover doesn't replay the animation over the already-visible page.
services.AddBitBrouterServices(o =>
{
o.ViewTransitions = true;
// o.ViewTransitionDefaultAnimations = true; // built-in glide/fade set
// o.ViewTransitionRespectReducedMotion = true; // crossfade under prefers-reduced-motion
});Shared-element morphs are pure CSS
Give the outgoing and incoming elements the same standard
view-transition-name and the browser morphs one into the other - no router
API involved. The gallery does exactly this: each grid tile and the detail hero share a
per-item name, so the tile appears to grow into the hero. This page's header is pinned the
same way: naming it gives it its own transition group, excluding persistent chrome from
the page animation.
/* the grid tile ... */ .tile-42 { view-transition-name: bb-item-42; } /* ... and the detail hero share one name - the browser does the rest */ .hero-42 { view-transition-name: bb-item-42; }
Overriding the defaults
The built-in animations live in the CSS layer bit-brouter, and unlayered
author CSS always wins over layered rules - so overriding is just writing a normal
::view-transition-* rule in your stylesheet, no !important
needed. Scope by direction with the data-brouter-nav attribute if forward and
back should differ.
/* your app.css - overrides the layered defaults automatically */ ::view-transition-new(root) { animation: 400ms ease both my-entrance; } html[data-brouter-nav="pop"]::view-transition-new(root) { animation-name: my-back-entrance; }
Accessibility is handled for you: with
ViewTransitionRespectReducedMotion (default), users with
prefers-reduced-motion get opacity cross-fades instead of movement. This demo
turns that respect off only because VMs and remote desktops often report reduced
motion spuriously - keep it on in production.