View Transitions
EmberKit integrates the View Transitions API for smooth SPA navigation. Transitions wait until route HTML is written into the app root before the browser captures snapshots.
Quick start
Enable transitions when bootstrapping the client router:
// src/index.tsx
import { render } from '@emberkit/core';
import App from './routes/_layout.tsx';
import { routes } from 'virtual:emberkit-routes';
render(App, document.getElementById('app')!, {
routes,
viewTransitions: true,
});
Internal links are intercepted in the capture phase so navigation runs once inside startViewTransition.
Programmatic navigation
import { navigate, navigateWithViewTransition } from '@emberkit/core';
// Per navigation
await navigate('/docs/api', { viewTransition: true });
// Dedicated helper
await navigateWithViewTransition('/docs/api');
CSS
Style the default cross-fade on the root:
::view-transition-old(root) {
animation: fade-out 0.3s ease-in-out forwards;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease-in-out forwards;
}
This docs site uses similar rules in src/styles/globals.css.
API reference
| Export | Description |
|---|---|
supportsViewTransitions() | Whether document.startViewTransition exists |
withViewTransition(callback) | Wraps async work in a transition when supported |
waitForAppUpdate(href, options?) | SPA navigation that resolves after #app mutates |
initViewTransitions(options?) | Link click interceptor (called automatically by render) |
navigateWithViewTransition(href, options?) | Navigate with transition + DOM update wait |
render() option:
viewTransitions?: boolean | { rootId?: string };
Use rootId if your mount element is not id="app".
How timing works
- User clicks an internal link (or you call
navigatewithviewTransition: true). - EmberKit starts a view transition.
history.pushState(orreplaceState) runs the patched router.renderCurrentRoute()updates the app root’s HTML.- A
MutationObserveron the app root resolves when child nodes change. - The browser animates from old snapshot to new snapshot.
This avoids flashing empty content between routes.
Opt out per link
Add data-no-transition on an anchor, or use modifier keys (Ctrl/Cmd click). External links, target="_blank", and same-page hash-only links are skipped automatically.
Browser support
| Browser | Support |
|---|---|
| Chrome / Edge 111+ | Full API |
| Safari / Firefox | Falls back to instant navigation (no errors) |
Users with prefers-reduced-motion should get reduced motion from the browser.
Docs site pattern
This app uses a thin wrapper so every useNavigate() call enables transitions by default:
// apps/docs/src/hooks/useNavigate.ts
import { navigate as coreNavigate } from '@emberkit/core';
export function useNavigate() {
return (path: string, options = {}) =>
coreNavigate(path, { ...options, viewTransition: options.skipTransition ? false : true });
}