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

ExportDescription
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

  1. User clicks an internal link (or you call navigate with viewTransition: true).
  2. EmberKit starts a view transition.
  3. history.pushState (or replaceState) runs the patched router.
  4. renderCurrentRoute() updates the app root’s HTML.
  5. A MutationObserver on the app root resolves when child nodes change.
  6. The browser animates from old snapshot to new snapshot.

This avoids flashing empty content between routes.

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

BrowserSupport
Chrome / Edge 111+Full API
Safari / FirefoxFalls 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 });
}