git:20260328.03e71dd to git:20260906.d80c3e7

172 added, 141 removed. Audit A to A.

---
name: web-animation-view-transitions
- description: View Transitions API patterns - same-document transitions, cross-document MPA transitions, shared element animations, pseudo-element styling, accessibility
+ description: View Transitions API patterns - same-document transitions, cross-document navigation, shared element animations, pseudo-element styling, transition types, reduced-motion handling
---
# View Transitions API Patterns
- > **Quick Guide:** Use the View Transitions API for native page/state transitions. `document.startViewTransition()` for same-document, `@view-transition { navigation: auto }` for cross-document MPA. Always feature-detect before use and respect `prefers-reduced-motion`. Use the options form `startViewTransition({ update, types })` when you need typed transitions.
+ > **Quick Guide:** The browser screenshots the old state, holds the new one live, and animates
+ > between them as a tree of pseudo-elements. `document.startViewTransition()` drives a same-document
+ > change; `@view-transition { navigation: auto }` on both pages drives a cross-document one.
+ > `view-transition-name` pulls an element out of the root snapshot so it can travel on its own, and
+ > the whole animation is customised in CSS rather than in script.
+ **Detailed Resources:**
+
+ - [examples/core.md](examples/core.md) — feature detection, state transitions, the three promises, skipping, CSS overrides
+ - [examples/spa.md](examples/spa.md) — theme reveal, form steps, tab panels, accordions, list reordering, reduced motion
+ - [examples/shared-elements.md](examples/shared-elements.md) — hero animations, multi-element cards, cross-document pairs, modals
+ - [reference.md](reference.md) — naming rules, pseudo-element tree, browser support, API tables
+
---
- <critical_requirements>
+ ## Which path applies
- ## CRITICAL: Before Using This Skill
+ - **Client routing or in-page state, with the DOM updated by script** — `startViewTransition()` takes
+ the update as a callback and captures around it; follow [examples/spa.md](examples/spa.md).
+ - **Server-rendered pages navigating to each other** — no script at all: both documents opt in with
+ `@view-transition { navigation: auto }`, and names are set through the `pageswap` and `pagereveal`
+ events; follow [examples/shared-elements.md](examples/shared-elements.md).
+ - **One element has to travel between the two states rather than cross-fade with everything else** —
+ it needs a `view-transition-name` matched on both sides; follow
+ [examples/shared-elements.md](examples/shared-elements.md).
- > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
+ ---
- **(You MUST feature-detect before using startViewTransition - it is NOT available in all browsers)**
+ <critical_requirements>
- **(You MUST respect prefers-reduced-motion by providing reduced or disabled animations)**
+ ## Before writing View Transitions code
- **(You MUST ensure view-transition-name values are unique - duplicate names break transitions)**
+ **Guard `startViewTransition` behind a support check, and run the update either way.** The call is
+ absent rather than inert where the API is unimplemented, so an unguarded call throws and the DOM
+ change never happens at all.
- **(You MUST clean up dynamically assigned view-transition-name values after transitions complete)**
+ **Give each participating element a name no other visible element carries.** The name identifies one
+ snapshot; two visible elements claiming the same one abort the whole transition, not just their own
+ part of it.
- **(You MUST use named constants for all animation timing values - NO magic numbers)**
+ **Clear a dynamically assigned `view-transition-name` once `transition.finished` resolves.** A name
+ left on an element collides with the next transition that assigns it, which is why the failure shows
+ up on the second navigation rather than the first.
+ **Give every customised transition a `prefers-reduced-motion` branch.** A page-level transition moves
+ the entire viewport, which is the class of motion the preference exists for; collapse the duration or
+ skip the transition and apply the update directly.
+
</critical_requirements>
---
- **Auto-detection:** View Transitions API, startViewTransition, view-transition-name, @view-transition, ::view-transition, pageswap, pagereveal, ViewTransition, view-transition-class, match-element, active-view-transition-type
+ **Auto-detection:** startViewTransition, view-transition-name, view-transition-class,
+ @view-transition, ::view-transition-old, ::view-transition-new, ::view-transition-group,
+ ::view-transition-image-pair, :active-view-transition-type, pageswap, pagereveal, updateCallbackDone,
+ skipTransition, match-element
- **When to use:**
+ **Applies to:**
- - Animating state changes in single-page applications
- - Creating smooth page-to-page transitions in multi-page applications
- - Implementing shared element (hero) animations between views
- - Providing visual continuity during navigation
- - Creating custom transition effects (slide, scale, circular reveal)
+ - State changes large enough that the whole view, or a large region of it, is replaced
+ - Page-to-page navigation in server-rendered sites
+ - Hero animations where one element persists across two views
+ - Direction-aware navigation, where forward and back need opposite motion
+ - Reordering, where elements move rather than change
- **Key patterns covered:**
+ **Handled elsewhere:**
- - Same-document transitions with startViewTransition()
- - Cross-document MPA transitions with @view-transition CSS
- - view-transition-name for shared element animations
- - Pseudo-element styling (::view-transition-old, ::view-transition-new)
- - Direction-aware transitions with :active-view-transition-type()
- - Feature detection and graceful fallbacks
- - prefers-reduced-motion accessibility patterns
+ - Hover, focus and pressed feedback on a single control — nothing is being replaced, so there is no
+ before-and-after pair to capture
+ - Motion whose velocity carries across an interruption, such as a spring picked up mid-gesture
+ - Frame-level timeline authoring, where the deliverable is a scrubbable timeline
- **When NOT to use:**
+ ---
- - Complex physics-based animations (use animation libraries)
- - Animations requiring precise timeline control
- - Simple hover/focus effects (use CSS transitions)
+ <philosophy>
- **Detailed Resources:**
+ A view transition is a screenshot and a live view, animated against each other. Everything follows
+ from that: the old side is inert pixels, so anything moving inside it freezes; naming an element
+ lifts it into its own snapshot pair with its own animation; and the whole tree is styled with CSS
+ because it is a tree of pseudo-elements, not a script-driven animation.
- - [examples/core.md](examples/core.md) - Feature detection, state transitions, promise handling, CSS customization
- - [examples/spa.md](examples/spa.md) - Theme switcher, form steps, tab panels, list reordering
- - [examples/shared-elements.md](examples/shared-elements.md) - Hero animations, multiple shared elements, MPA shared elements, modals
- - [reference.md](reference.md) - Decision frameworks, pseudo-element reference, browser support, anti-patterns
+ The DOM update itself stays ordinary. `startViewTransition` wraps a change that would have happened
+ anyway, which is why the fallback for an unsupported browser is simply making the change.
+ </philosophy>
+
---
- <philosophy>
+ <decision_framework>
- ## Philosophy
+ ## Which snapshot the element belongs to
- The View Transitions API provides a native browser mechanism for creating animated transitions between DOM states or pages. It captures "before" and "after" snapshots, overlays them as pseudo-elements, and animates between them.
+ ```
+ Does an element exist on both sides and represent the same thing?
+ ├─ YES -> give it one view-transition-name on both sides; it gets its own group and travels
+ └─ NO -> leave it in the root snapshot; it cross-fades with everything else around it
+ ```
- **Core principles:**
+ Each named element adds a group, an image pair and two snapshots to the tree, so naming everything
+ costs more than it buys — name what a reader would follow with their eyes.
- 1. **Native over library** - Browser-native transitions are more performant and require less JavaScript
- 2. **Progressive enhancement** - Always feature-detect and provide functional fallback
- 3. **Snapshot-based** - Old state is captured as a screenshot, new state as a live representation
- 4. **CSS-driven** - Customize animations through pseudo-element CSS, not JavaScript
- 5. **Accessibility-first** - Always respect prefers-reduced-motion user preferences
+ ## Default or custom animation
- </philosophy>
+ ```
+ Cross-fade at a different duration -> set animation-duration on ::view-transition-old/new(root)
+ Slide, scale, wipe -> keyframes on the old and new pseudo-elements
+ Direction-dependent motion -> a transition type, selected with :active-view-transition-type()
+ Geometry computed at the moment of the transition (a reveal from a click point)
+ -> await transition.ready, then animate the pseudo-element
+ ```
+ ## Where the name is set
+
+ CSS is right for an element that is unique on the page — a header, a hero, a single panel. Script is
+ right for anything appearing more than once, because a static rule over a list gives every row the
+ same name and breaks the transition; assign per element before the transition and clear after it, or
+ use `view-transition-name: match-element` where support allows.
+
+ </decision_framework>
+
---
<patterns>
- ## Core Patterns
+ ## Core patterns
### Pattern 1: Feature Detection with Fallback
- Always check for API support before using View Transitions. See [examples/core.md](examples/core.md) Pattern 1 for full utility.
-
```typescript
const SUPPORTS_VIEW_TRANSITIONS =
typeof document !== "undefined" && "startViewTransition" in document;
function updateWithTransition(updateFn: () => void | Promise<void>): void {
if (!SUPPORTS_VIEW_TRANSITIONS) {
updateFn();
return;
}
document.startViewTransition(() => updateFn());
}
```
- **Why good:** Prevents runtime errors in unsupported browsers, provides seamless fallback
+ The `typeof document` guard matters during server rendering, where there is no `document` to probe.
+ Full code: [examples/core.md](examples/core.md)
+
---
- ### Pattern 2: Same-Document (SPA) Transitions
+ ### Pattern 2: Same-Document Transitions
- Animate DOM state changes within a single page. See [examples/core.md](examples/core.md) Patterns 2-5 for state transitions, async loading, promise handling, and skip logic.
+ The callback performs the DOM update; the browser captures before calling it and again after it
+ resolves.
```typescript
- // startViewTransition accepts a callback or an options object
const transition = document.startViewTransition(async () => {
await updateFn();
});
- // Options form - set types for CSS targeting
+ // Options form: classify the transition for CSS to select on
const transition = document.startViewTransition({
update: () => updateDOM(),
types: ["slide-forward"],
});
await transition.finished;
```
- **ViewTransition object provides three promises:**
+ | Promise | Resolves when |
+ | ------------------------------- | ---------------------------------------------------- |
+ | `transition.updateCallbackDone` | The DOM update callback has finished |
+ | `transition.ready` | The pseudo-element tree exists and animation is next |
+ | `transition.finished` | The animation is over and the new view is live |
- | Promise | Resolves when |
- | ------------------------------- | ----------------------------- |
- | `transition.ready` | Pseudo-element tree created |
- | `transition.updateCallbackDone` | DOM update callback completed |
- | `transition.finished` | Animation complete |
+ An `async` callback holds the page frozen until it resolves, so anything slow inside it is a visible
+ stall — update to a loading state rather than awaiting a network round trip.
- ---
+ Full code: [examples/core.md](examples/core.md)
- ### Pattern 3: Cross-Document (MPA) Transitions
+ ---
- Enable transitions between separate pages without JavaScript. Both pages must opt in.
+ ### Pattern 3: Cross-Document Transitions
```css
- /* Include on BOTH source and destination pages */
+ /* on both the source and the destination document */
@view-transition {
navigation: auto;
}
```
- **Why good:** No JavaScript required, works for traverse/push/replace navigations
+ No script and no router involvement; it covers push, replace and traverse navigations within one
+ origin. Only one side opting in yields no transition at all.
- **Obsolete syntax:** `<meta name="view-transition" content="same-origin">` - use the CSS at-rule instead.
+ Full code: [examples/shared-elements.md](examples/shared-elements.md)
---
### Pattern 4: Shared Element Transitions
- Create hero animations by giving matching elements the same `view-transition-name`. See [examples/shared-elements.md](examples/shared-elements.md) for full product list-to-detail, multi-element, and MPA examples.
+ Matching names on either side of the change make one element travel instead of cross-fading.
```css
- :root {
- --hero-duration: 300ms;
- --hero-easing: cubic-bezier(0.4, 0, 0.2, 1);
- }
-
- /* Same name on both pages/states creates shared element animation */
.product-thumbnail {
view-transition-name: product-hero;
}
.product-image {
view-transition-name: product-hero;
}
::view-transition-group(product-hero) {
animation-duration: var(--hero-duration);
animation-timing-function: var(--hero-easing);
}
```
- **Key rules:** Names must be unique across the document. Clean up dynamically assigned names after `transition.finished`.
+ The group animates position and size; the image pair cross-fades the two snapshots inside it. Where
+ the two sides differ in aspect ratio, `object-fit` on the old and new pseudo-elements stops the
+ snapshot squashing.
+ Full code: [examples/shared-elements.md](examples/shared-elements.md)
+
---
### Pattern 5: Custom CSS Animations
- Override default cross-fade with custom animations via pseudo-elements. See [examples/core.md](examples/core.md) Pattern 6 for full examples.
+ The default cross-fade is a UA stylesheet animation on the old and new pseudo-elements; overriding it
+ is ordinary CSS.
```css
- :root {
- --transition-duration: 300ms;
- --transition-easing: ease-in-out;
- }
-
::view-transition-old(root) {
animation: slide-out-left var(--transition-duration) var(--transition-easing);
}
::view-transition-new(root) {
animation: slide-in-right var(--transition-duration) var(--transition-easing);
}
```
- **Why good:** CSS custom properties for timing constants, GPU-accelerated transforms
+ Full code: [examples/core.md](examples/core.md)
---
### Pattern 6: Direction-Aware Transitions
- Use different animations for forward vs backward navigation. Use the `types` parameter or `ViewTransition.types` set.
+ A transition type is a label the CSS can select on, so forward and back share one set of keyframes
+ and differ only in which pair is applied.
```css
html:active-view-transition-type(forwards) {
&::view-transition-old(content) {
animation-name: slide-out-left;
}
&::view-transition-new(content) {
animation-name: slide-in-right;
}
}
html:active-view-transition-type(backwards) {
&::view-transition-old(content) {
animation-name: slide-out-right;
}
&::view-transition-new(content) {
animation-name: slide-in-left;
}
}
```
```typescript
- // Preferred: set types via options parameter
document.startViewTransition({
update: () => navigateForward(),
types: ["forwards"],
});
-
- // Alternative: mutate types set on existing transition
- const transition = document.startViewTransition(updateFn);
- transition.types.add("forwards");
```
- See [examples/spa.md](examples/spa.md) for form step and tab panel examples.
+ `transition.types` is a mutable set on an existing transition, for the case where the direction is
+ only known after the update has started.
- ---
+ Full code: [examples/spa.md](examples/spa.md)
- ### Pattern 7: Accessibility - Reduced Motion
+ ---
- Always respect user preferences for reduced motion.
+ ### Pattern 7: Reduced Motion
```css
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 0.01ms !important;
}
}
```
```typescript
const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
function shouldEnableTransitions(): boolean {
if (window.matchMedia(REDUCED_MOTION_QUERY).matches) return false;
return "startViewTransition" in document;
}
```
- See [examples/spa.md](examples/spa.md) for a full accessible transition wrapper with preference change monitoring.
+ The CSS form collapses the animation while keeping the transition machinery, which is enough for a
+ cross-fade. The script form skips the transition entirely, which is the honest answer for a
+ full-viewport slide or a reveal. Between them sits a third: cancel the animation on
+ `::view-transition-group(*)`, which is what carries the size and position change, and leave the old
+ and new pseudo-elements cross-fading. That drops the travel without dropping the feedback.
+ Full code: [examples/spa.md](examples/spa.md)
+
---
- ### Pattern 8: Circular Reveal Effect
+ ### Pattern 8: Geometry Computed at Transition Time
- Advanced custom animation using Web Animations API. Must `await transition.ready` before animating pseudo-elements.
+ Where the animation depends on something known only at the moment of the click, await
+ `transition.ready` and animate the pseudo-element directly.
```typescript
- const REVEAL_DURATION_MS = 400;
- const REVEAL_EASING = "ease-in-out";
-
const transition = document.startViewTransition(updateFn);
await transition.ready;
document.documentElement.animate(
{
clipPath: [`circle(0 at ${x}px ${y}px)`, `circle(${r}px at ${x}px ${y}px)`],
},
{
duration: REVEAL_DURATION_MS,
easing: REVEAL_EASING,
pseudoElement: "::view-transition-new(root)",
},
);
```
- See [examples/spa.md](examples/spa.md) for a complete theme-switcher circular reveal implementation.
+ The animation targets the document element with a `pseudoElement` option, because the pseudo-element
+ has no node of its own to address.
+ Full code: [examples/spa.md](examples/spa.md)
+
</patterns>
---
<red_flags>
- ## RED FLAGS
-
- **High Priority Issues:**
-
- - **Missing feature detection** - Calling `startViewTransition()` without checking support crashes in older browsers
- - **Duplicate view-transition-name values** - Two visible elements with the same name breaks the transition entirely
- - **Not cleaning up dynamic names** - Leftover names cause conflicts in subsequent transitions
- - **Ignoring prefers-reduced-motion** - Mandatory for accessibility; always provide reduced or no animation
- - **Magic numbers for timing** - All duration/delay values must be named constants or CSS custom properties
+ ## Red flags
- **Medium Priority Issues:**
+ **Breaks at runtime:**
- - **Using obsolete meta tag syntax** - `<meta name="view-transition">` is deprecated; use `@view-transition` CSS
- - **Not awaiting transition.ready for custom animations** - Web Animations API must wait for pseudo-elements to exist
- - **Missing @view-transition on both MPA pages** - Cross-document transitions require opt-in on source AND destination
- - **Setting view-transition-name in CSS for dynamic lists** - Causes name conflicts; use JavaScript assignment or `match-element`
+ - `document.startViewTransition(...)` with no support check — throws where the API is absent, so the
+ DOM update is lost along with the animation — branch on `"startViewTransition" in document`
+ - Two visible elements carrying the same `view-transition-name` — the whole transition is abandoned,
+ including the parts that were correct — assign per element from script, or use `match-element`
+ - A `view-transition-name` set in CSS on a list selector — every row claims the same name — set it
+ from script before the transition and clear it after
+ - A name left assigned after `transition.finished` — collides on the next transition, so the failure
+ appears one navigation later than the cause — clear it in the `finished` handler
+ - `@view-transition` on only one of the two documents — cross-document transitions need both sides to
+ opt in — add the at-rule to the destination as well
+ - Animating a pseudo-element before `transition.ready` resolves — the tree does not exist yet and the
+ animation silently targets nothing — await the promise first
+ - No `prefers-reduced-motion` branch — a full-viewport slide reaches users who asked for none — add
+ the media query, or skip the transition in script
+ - `<meta name="view-transition" content="same-origin">` — obsolete and ignored — use the
+ `@view-transition` at-rule
- **Gotchas & Edge Cases:**
+ **Surprising behaviour:**
- - **Old state is a screenshot** - Videos, animations, GIFs freeze in the old snapshot
- - **New state is "live"** - Interactive content continues playing in the new snapshot
- - **Transition names are global** - Same name on different page sections will conflict
- - **Animations block interaction** - User cannot interact until transition completes; keep animations under 300ms
- - **Cross-document needs same-origin** - Different origins cannot share transitions
- - **`match-element` requires Chrome 137+/Safari 18.4+** - Not yet available in Firefox
- - **`pagereveal` must be registered early** - Put handler in `<head>` or use `blocking="render"`
- - **Reserved names** (`auto`, `inherit`, `none`, `unset`) are CSS keywords, not valid custom identifiers
+ - The old side is a screenshot, so video, GIFs and running animations freeze in it; the new side is
+ live and keeps playing
+ - Names are document-global, so the same name on two unrelated sections conflicts even when they are
+ far apart
+ - The page is inert for the duration of the transition, which puts a long animation directly into
+ interaction latency — under 300ms for anything on a navigation path
+ - Cross-document transitions require same-origin navigation
+ - `match-element` needs Chrome 137+ or Safari 18.4+ and is unavailable in Firefox
+ - A `pagereveal` handler registered late misses the event — put it in `<head>`, or mark the script
+ `blocking="render"`
+ - `auto`, `inherit`, `none` and `unset` are CSS-wide keywords rather than custom identifiers, so they
+ cannot be used as names
</red_flags>
-
- ---
-
- <critical_reminders>
-
- ## CRITICAL REMINDERS
-
- > **All code must follow project conventions in CLAUDE.md**
-
- **(You MUST feature-detect before using startViewTransition - it is NOT available in all browsers)**
-
- **(You MUST respect prefers-reduced-motion by providing reduced or disabled animations)**
-
- **(You MUST ensure view-transition-name values are unique - duplicate names break transitions)**
-
- **(You MUST clean up dynamically assigned view-transition-name values after transitions complete)**
-
- **(You MUST use named constants for all animation timing values - NO magic numbers)**
-
- **Failure to follow these rules will break transitions in unsupported browsers and create inaccessible experiences.**
-
- </critical_reminders>