Skip to content

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.