Route transition
Live demo
Section titled “Live demo”The page below is the real demo.html from the kit. Use its “Toggle dark” button for the dark theme.
Current rule (v2.0.1, 2026-11-13): Route exit is now base (180 ms), enter slow (240 ms), with a token-derived quarter-fast lead (30 ms). Both use the same route origin. Latest navigation owns cleanup; focus moves to the main region only for pathname changes. Supersedes the retained implementation/timing claims below; see docs/current-rules.md#cr-motion-01 and motion-flow.
A page change reads as one move: only the main content region fades out (90 ms) and the new page fades in with a short rise, while the sidebar and top bar stay still. It uses the View Transitions API on the one named region and falls back to a CSS fade where the API is missing.
Open demo.html in a browser (it links ../../tokens/tokens.css) and use “Toggle dark” to check both themes.
| File | What it is |
|---|---|
route-transition.css |
The View Transition rules for the named main region, the CSS fallback (.sw-route-enter) and the direction distances, verbatim from the app. |
demo.html |
A standalone page with the live animation in light and dark, replayable. |
react/ |
Copies of the real app files (see react/README.md for the imports to replace). |
previews/ |
Animated WebP and GIF previews, light and dark (regenerated by scripts/make-previews.mjs). |
Where the app uses it
Section titled “Where the app uses it”- Every in-app link and navigation:
components/layouts/,components/shell/(sidebar, bottom tabs, top bar), staff, member, public and docs pages. Only the element markeddata-sw-mainanimates. - Files:
lib/motion/TransitionLink.tsx,useTransitionNavigate.ts,routeTransition.ts,RouteTransitionGate.tsx,ScrollManager.tsx,styles/motionRoute.css.
The visitor sees that the page changed without losing their place: the chrome is stable, the content is new. The old page leaves before the new one arrives, so the two are never visible together. Direction sets the settle distance (forward rises 8 px, a lateral move 6 px, back sinks 6 px, a fade-only change 0).
Timing, easing and distance
Section titled “Timing, easing and distance”Current rule (v2.0.1, 2026-11-13): The retained table/snippet below records the original app copy. Use the current executable files and the complete flow recipe for decision 399; see current rules.
| What | Value |
|---|---|
| Old page out | 90 ms, --sw-ease-exit, opacity only |
| New page in | --sw-dur-slow (240 ms), --sw-ease-enter, after 70 ms, opacity and translateY |
| Distance | --sw-vt-y: 8 px forward, 6 px lateral, -6 px back, 0 fade; kiosk and TV use 0 |
| Safety | the transition settles after 160 ms if the page is slow, and never blocks navigation |
Trigger
Section titled “Trigger”A plain left-click on a TransitionLink or a push navigation through useTransitionNavigate sets data-sw-vt="route" and data-sw-nav="forward|lateral|back|fade" on <html> and calls document.startViewTransition. Back and forward buttons and unwrapped links replay .sw-route-enter instead.
Reduced motion
Section titled “Reduced motion”No View Transition is started and .sw-route-enter is inside no-preference, so the new page simply appears. Scroll is restored instantly.
Dark theme
Section titled “Dark theme”Colour-neutral: the transition animates opacity and transform of whatever the page already paints, so nothing differs between themes.
Do and don’t
Section titled “Do and don’t”- Do mark one region
data-sw-mainand keep the sidebar and top bar outside it. - Do keep the whole move under 240 ms plus the 70 ms offset.
- Do not animate the sidebar, the top bar or the browser chrome.
- Do not block navigation on the animation or wait for it before fetching.
- Do not slide or scale on kiosk and TV screens (fade only).
Copy-paste usage
Section titled “Copy-paste usage”Current rule (v2.0.1, 2026-11-13): The retained table/snippet below records the original app copy. Use the current executable files and the complete flow recipe for decision 399; see current rules.
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;function go(update) { if (!document.startViewTransition || reduced) return update(); document.documentElement.dataset.swNav = "forward"; // forward | lateral | back | fade document.documentElement.dataset.swVt = "route"; const t = document.startViewTransition(update); t.finished.finally(() => { delete document.documentElement.dataset.swVt; delete document.documentElement.dataset.swNav; });}Mark the region <main data-sw-main>. Without View Transitions, add the class sw-route-enter (and data-sw-enter="forward") to it after swapping the content.
Every other animation in the product, and the rules behind them: ../../docs/motion.md. Index of the kit: ../README.md.
Flow contract (decision 399)
Section titled “Flow contract (decision 399)”Route exit is now base (180 ms), enter slow (240 ms), with a token-derived quarter-fast lead (30 ms). Both use the same route origin. Latest navigation owns cleanup; focus moves to the main region only for pathname changes.
Complete rules, sequencing, keyboard and verification. The demo includes real static posters and explicit image Play/Pause controls.
Local token steps for batch 1B
Section titled “Local token steps for batch 1B”These aliases are local additions; global tokens are unchanged. Promote through the token batch when adopted.
| Local custom property | Definition |
|---|---|
--sw-local-duration-70 |
calc(var(--sw-dur-fast) * 0.58333333) |
--sw-local-duration-90 |
calc(var(--sw-dur-fast) * 0.75000000) |
Source
Section titled “Source”Copied from motion/route-transition/ on every build: the stylesheet, demo.html, react/ copies of the app files and the previews/.

