--- title: 'View Transitions' slug: view-transitions description: 'Animate full-page navigations with the browser cross-document View Transitions API — zero JavaScript.' --- ## View Transitions `` opts your app into the browser's cross-document [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API), animating full-page navigations with **zero client JavaScript**. Mochi is an MPA — every navigation is a real page load — so the browser does the work; you just declare the animation. Render it from a component that appears on **every** page (both the page you leave and the one you land on must opt in), e.g. a shared page shell component: ```svelte ... ``` Navigate between pages and they crossfade. That's the whole setup. Render exactly **one** `` per page — two instances would emit the same global `@keyframes` names and competing rules. If a second one renders anyway (say, one from a layout and one from a page), it logs a warning and emits nothing; the first instance wins. Don't hydrate it: the component emits static CSS and no markup, so there's nothing for `mochi:hydrate` / `mochi:defer` to do — it throws if invoked as an island. ### Props | Prop | Type | Default | Description | | ---------------------- | -------------------------------------------------- | -------- | ----------------------------------------------------------------------- | | `type` | `'fade' \| 'slide' \| 'scale' \| 'blur' \| 'flip'` | `'fade'` | The transition preset. | | `custom` | `{ out?: string; in?: string }` | — | Custom keyframe bodies for the leaving/entering page. Overrides `type`. | | `duration` | `number` (ms) | `250` | Animation duration. | | `easing` | `string` | `'ease'` | The animation timing function. | | `regions` | `string \| string[]` | — | Confine the animation to elements with these `view-transition-name`s. | | `keepElementSelectors` | `string \| string[]` | — | CSS selectors for persistent chrome to hold still across navigations. | ```svelte ``` Five presets ship built in: `fade` (crossfade), `slide` (horizontal translate), `scale` (zoom in/out), `blur` (focus pull), and `flip` (3D Y-axis rotation). They all animate the page root, so they apply to any page with no per-element setup, and reduced-motion users get no animation automatically. Need a motion none of the presets cover? Reach for `custom` (below). ### Custom transitions Pass `custom` to bring your own animation. `out` and `in` are the **body** of each keyframe — the `from` / `to` / `%` rules — for the page you leave and the page you land on; Mochi wraps each into an `@keyframes` for you: ```svelte ``` Either side is optional — supply just `in` or just `out` and the other direction won't animate. `custom` composes with `duration`, `easing`, `regions`, `keepElementSelectors`, and reduced-motion exactly like the presets do. When `custom` is set it **overrides** `type` — the preset is ignored, so you don't need to omit `type`. ### Animating only part of the page The View Transitions API always snapshots the **whole viewport** — you can't restrict the capture to a subtree. What you can scope is _which parts animate_. Pass `regions` to confine the transition to elements you've given a [`view-transition-name`](https://developer.mozilla.org/en-US/docs/Web/CSS/view-transition-name); everything else swaps instantly instead of cross-fading. ```svelte
``` ```svelte ``` An empty array (`regions={[]}`) disables the animation entirely — everything swaps instantly. Each `view-transition-name` must be **unique per document** — don't reuse one name across several elements on the same page. Cross-document view transitions are supported in current Chromium browsers. Where unsupported the navigation just happens with no animation — nothing to polyfill, nothing breaks. ### Keeping elements still By default the whole page crossfades. To hold persistent chrome — a banner, sidebar, or header — still instead, pass `keepElementSelectors` a list of CSS selectors. Each matched element is lifted out of the page crossfade and frozen (both its position and its snapshots) while the rest of the page transitions: ```svelte ``` `keepElementSelectors` assigns each selector a unique `view-transition-name` and emits the freeze CSS for you, so there's nothing to hand-write. Render the same list on every page — i.e. from your shared layout — so the page you leave and the page you land on agree. Each selector must match **exactly one** element per page. `view-transition-name`s are unique per document, so a selector that matches several elements breaks the transition. If you'd rather wire it by hand — or need finer control — give the element a [`view-transition-name`](https://developer.mozilla.org/en-US/docs/Web/CSS/view-transition-name) in your own CSS and zero its animations; that's exactly what `keepElementSelectors` generates: ```css .sidebar { view-transition-name: sidebar; } ::view-transition-group(sidebar), ::view-transition-old(sidebar), ::view-transition-new(sidebar) { animation: none; } ```