View Transitions
Problem
The View Transition API requires wrapping every history push in document.startViewTransition(). Integrating this manually with a client-side router creates race conditions between animation callbacks and navigation state updates.
Solution
Set viewTransition: true at router creation. Route automatically wraps each navigation commit in document.startViewTransition(). Override per navigation with { viewTransition: false }.
ts
import { createRouter } from '@vielzeug/wayfinder';
const router = createRouter({
viewTransition: true,
routes: {
home: { path: '/' },
settings: { path: '/settings' },
},
notFound: { component: NotFoundPage },
});
await router.navigate({ name: 'settings' }); // uses transition
await router.navigate({ name: 'home' }, { viewTransition: false }); // skips transitionOptional CSS:
css
::view-transition-old(root) {
animation: fade-out 160ms ease;
}
::view-transition-new(root) {
animation: fade-in 160ms ease;
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}Pitfalls
document.startViewTransitionis not available in all browsers and is absent in Node.js test environments. Wayfinder falls back to a plain navigation silently, so tests always pass.- Transitions run synchronously during the commit phase of navigation. Heavy CSS animations block the next interaction until the transition resolves.
- Per-navigation
{ viewTransition: false }still runsdocument.startViewTransitionif the router default istrue; it means the transition is skipped for the DOM swap, not the API call.