Use when a single-page-app DOM swap needs a Shared-Element transition (old and new states cross-fade with named elements morphing between them) without rolling a custom FLIP library, when a multi-page-app navigation should animate seamlessly from one document to another via `@view-transition { navigation: auto }`, when a hero element should animate as it enters the viewport (parallax, fade-up, scale-in) without `requestAnimationFrame` or `IntersectionObserver` glue, when a scroll progress indicator at the top of the page must track exactly how far down the user has scrolled, when a horizontal carousel needs `scroll-snap-type: x mandatory` so cards snap into place, when `background-attachment: fixed` parallax is being considered (it is a mobile compositor disaster and must be replaced with scroll-driven animations), or when an animation must respect `prefers-reduced-motion: reduce`. Prevents the cross-document `@view-transition` declared on only one side (the transition silently no-ops; MUST be declared on bot
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Use when a single-page-app DOM swap needs a Shared-Element transition (old and new states cross-fade with named elements morphing between them) without rolling a custom FLIP library, when a multi-page-app navigation should animate seamlessly from one document to another via `@view-transition { navigation: auto }`, when a hero element should animate as it enters the viewport (parallax, fade-up, scale-in) without `requestAnimationFrame` or `IntersectionObserver` glue, when a scroll progress indicator at the top of the page must track exactly how far down the user has scrolled, when a horizontal carousel needs `scroll-snap-type: x mandatory` so cards snap into place, when `background-attachment: fixed` parallax is being considered (it is a mobile compositor disaster and must be replaced with scroll-driven animations), or when an animation must respect `prefers-reduced-motion: reduce`. Prevents the cross-document `@view-transition` declared on only one side (the transition silently no-ops; MUST be declared on both source and destination documents), repeating `view-transition-name` across siblings on the same snapshot (spec collision; names MUST be unique per snapshot), `startViewTransition` shipped without a `prefers-reduced-motion` check (motion-sensitive users get sudden animations they cannot disable), large `view-transition-name` snapshots painting the whole page (the snapshot pipeline copies the named element's pixels each frame), scroll-driven animations shipped without an `@supports (animation-timeline: scroll())` gate (Limited Availability in 2026; absence of the gate means non-supporting engines see no animation at all), `scroll-snap-type` set on a container without `scroll-snap-align` on the children (nothing snaps), and `scroll-snap-type: mandatory` on long-form content (the user cannot scroll between snap points; use `proximity` instead). Covers the same-document View Transition API (`document.startViewTransition(callback)` returning a `ViewTransition` with `.ready` / `.finished` / `.updateCallbackDone` promises and a `.skipTransition()` method), the cross-document opt-in (`@view-transition { navigation: auto }` on BOTH source and destination), the `view-transition-name` CSS property and the four pseudo-elements (`::view-transition`, `::view-transition-group(<name>)`, `::view-transition-image-pair(<name>)`, `::view-transition-old(<name>)` / `::view-transition-new(<name>)`), scroll-driven animations via `animation-timeline: scroll(<axis>? <scroller>?)` (axes `block` / `inline` / `x` / `y`; scrollers `nearest` / `root` / `self`) and `view(<axis>? <inset>?)`, named timelines (`scroll-timeline: --name <axis>;` and `view-timeline: --name <axis> <inset>?;` plus `timeline-scope: --name` for distant ancestors), `scroll-snap-type` (axis + `mandatory` / `proximity`), `scroll-snap-align` (`start` / `center` / `end`), `scroll-snap-stop: always`, scroll-padding / scroll-margin for inset adjustment, the `scrollsnapchange` and `scrollsnapchanging` events, and the universal `@media (prefers-reduced-motion: reduce)` gate that turns animations off or reduces them to opacity-only. Keywords: View Transitions API, startViewTransition, ViewTransition object, ViewTransition ready, ViewTransition finished, ViewTransition updateCallbackDone, ViewTransition skipTransition, view-transition-name, view-transition-class, at-view-transition, navigation auto, view-transition pseudo, view-transition-group, view-transition-image-pair, view-transition-old, view-transition-new, active-view-transition pseudo-class, PageRevealEvent, PageSwapEvent, scroll-driven animations, animation-timeline, scroll function timeline, view function timeline, scroll-timeline, scroll-timeline-name, scroll-timeline-axis, view-timeline, view-timeline-name, view-timeline-axis, view-timeline-inset, timeline-scope, animation-range, scroll-snap-type, scroll-snap-align, scroll-snap-stop, scroll-padding, scroll-margin, scrollsnapchange, scrollsnapchanging, prefers-reduced-motion, FLIP animation, SPA route change, MPA navigation, parallax, page transition jank, view transition …
license
MIT
compatibility
Designed for Claude Code. Requires Frontend Design evergreen-2026.
This skill defines deterministic rules for shipping View Transitions (single-document and cross-document), CSS scroll-driven animations (animation-timeline: scroll() / view() and the named-timeline forms), and scroll-snap. All three are part of the modern declarative-animation stack ; they replace large categories of JavaScript glue (FLIP libraries, IntersectionObserver reveal-on-scroll, requestAnimationFrame scroll-position pollers) with browser-native primitives.
This skill builds on [[frontend-perf-animation-gpu-containment]] (the compositor-only rule that scroll-driven animations rely on) and [[frontend-a11y-motion-contrast-wcag22]] (the prefers-reduced-motion rule that gates every animation in this skill). It is referenced by [[frontend-visual-micro-interactions]] (which composes with these primitives) and [[frontend-impl-popover-dialog-anchor]] (which uses View Transitions for popover entrance / exit).
Each view-transition-name MUST be UNIQUE per snapshot. Repeating across simultaneously-visible siblings produces a spec collision and the engine drops the entire transition.
The name MUST be set on BOTH the old element (source state) and the new element (destination state) to morph between them.
Default animation : ::view-transition-old(*) fades out, ::view-transition-new(*) fades in. Customise per-name with the animation-* properties listed above.
Anonymous VIEW progress timeline tracking element visibility in its scroll container.
<dashed-ident>
Reference to a scroll-timeline-name or view-timeline-name declared elsewhere.
/* Page-wide scroll progress (a top-of-page progress bar) */.progress { animation: grow auto linear; animation-timeline: scroll(block root); transform-origin: left; }
@keyframes grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
/* Card fades in as it enters the viewport */.card { animation: fadeIn auto linear; animation-timeline: view(block); }
@keyframes fadeIn { from { opacity: 0; transform: translateY(2rem); } to { opacity: 1; transform: translateY(0); } }
Per MDN: animation-timeline (verified 2026-05-19), animation-timeline is RESET-ONLY in the animation shorthand. Declare animation-timeline AFTER animation: ..., never inside it.
Named timelines
.scroller { scroll-timeline: --story block; overflow-y: scroll; }
.indicator { animation: bar auto linear; animation-timeline: --story; }
.hero { view-timeline: --hero block; }
.hero-cta { animation: pop auto linear; animation-timeline: --hero; }
timeline-scope: --hero on a common ancestor lets distant descendants reference the same timeline.
view-timeline vs scroll-timeline
Property
Tracks
Use for
scroll-timeline
Scroll position of a SCROLLER (its scroll bar moves 0% to 100%).
Whole-document progress bars, parallax tied to total scroll.
view-timeline
VISIBILITY of a subject element as it crosses the scrollport.
Per-element reveal animations (fade-in, scale-in as the element enters view).
Scroll-snap
Per MDN: CSS scroll snap (verified 2026-05-19), Baseline Widely Available since April 2022.
none / x mandatory / y mandatory / both mandatory / x proximity / y proximity / both proximity
scroll-snap-align
snap children
none / start / center / end, optionally per-axis
scroll-snap-stop
snap children
normal / always (forces a stop at this snap target ; no overscroll past it)
scroll-padding
scroll container
adjusts the optimal viewing region (insets for sticky headers, etc.)
scroll-margin
snap children
adjusts the visual area of the snap target
Events :
scrollsnapchange : fires when a new snap target is selected.
scrollsnapchanging : fires when a snap target change is pending.
mandatory forces snapping ; the user cannot leave a snap point partially scrolled. proximity only snaps near the snap points ; safer for long-form content where free scroll is also valuable.
Reduced motion gate (universal)
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (transition && reduce) transition.skipTransition();
ALL animations in this skill MUST be gated. The skill assumes the user preference matters every time.
Decision Trees
Decision : startViewTransition or plain CSS animation?
What kind of state change are you animating?
A DOM swap : list view becomes detail view ; route change ;
filter applied to a card grid ; sort order changed.
-> document.startViewTransition(() => updateDOM()).
Optionally tag morphing elements with
view-transition-name so they cross-morph instead of
cross-fading.
A CSS-only state change : :hover, :focus, transition on a
property, data-state="open" toggling.
-> Plain CSS transition / animation. No view transition.
A page navigation (real <a href> to another document) in an MPA.
-> @view-transition { navigation: auto; } on BOTH documents.
NO JavaScript ; the browser orchestrates.
Same DOM swap but motion-sensitive user.
-> ALWAYS check prefers-reduced-motion first ; if reduce,
call updateDOM directly (no transition).
Decision : scroll-timeline or view-timeline?
What does "progress" mean for this animation?
Progress is how far down the page (or container) the user has
scrolled, from 0% (top) to 100% (bottom).
-> scroll-timeline (anonymous via animation-timeline: scroll(...)
or named via scroll-timeline: --name).
Progress is how much of a specific element has entered the
scrollport (the element being animated reveals as it crosses).
-> view-timeline (anonymous via animation-timeline: view(...) or
named via view-timeline: --name).
Multiple elements share the same timeline (e.g. a stacked-card
effect choreographed across siblings).
-> Named timeline. scroll-timeline / view-timeline on the
declaring element ; timeline-scope: --name on a common
ancestor ; animation-timeline: --name on the descendants.
Decision : scroll-snap-type: mandatory or proximity?
What kind of content is in the scroll container?
Discrete pages, cards, or slides that MUST land aligned (a
photo carousel, a paged scrolling document, a step-by-step
onboarding scroller).
-> mandatory. User cannot leave the container between snap
points.
Long-form content with anchor points (a documentation page where
you want sections to snap, but free scroll between paragraphs
also has value).
-> proximity. Only snaps when the user releases near a snap
point.
Scrolling should be entirely free, but the API helps anchor on
release.
-> none (default) plus scrollIntoView({ behavior: 'smooth' })
for programmatic snaps.
Decision : prefers-reduced-motion?
ALWAYS. No exceptions. Every animation in this skill MUST gate :
- View transition : check the media query before startViewTransition,
OR call transition.skipTransition() inside.
- Scroll-driven animation : @media (prefers-reduced-motion: reduce)
{ .x { animation: none; } } OR replace transform-based animation
with opacity-only.
- Scroll-snap : scroll-snap-type: none on reduce, OR drop to
proximity from mandatory (less likely to disorient).
Patterns
Pattern 1 : same-document view transition with named morph
Clicking a link to the detail page triggers a cross-document transition because both documents declare @view-transition and both name the same element hero.
Engines without animation-timeline show the static, fully-opaque state ; engines with support animate.
Anti-Patterns Index
See anti-patterns.md. Eight cataloged : background-attachment: fixed parallax ; @view-transition declared only on source ; startViewTransition without prefers-reduced-motion ; repeated view-transition-name across siblings ; scroll-snap-type without scroll-snap-align on children ; scroll-driven without @supports gate ; scroll-snap-type: mandatory on long-form content ; view-transition-name on a giant element (whole-page snapshot performance trap).
Reference Links
Methods and signatures : full API surface, pseudo-element tree, animation-timeline value matrix, named-timeline mechanics, scroll-snap properties.
Examples : renderable HTML demo combining a scroll progress bar, a fade-in card gallery, and a scroll-snap carousel; six additional patterns including cross-document MPA and named-timeline choreography.
Anti-patterns : eight cataloged anti-patterns with symptom, root cause, and fix.
Cross-references
[[frontend-perf-animation-gpu-containment]] : compositor-only animation rule and @property for animatable customs ; all scroll-driven animations should use transform and opacity only.
[[frontend-visual-micro-interactions]] : combines these primitives with easing curves for hover / press / focus choreography.
[[frontend-a11y-motion-contrast-wcag22]] : prefers-reduced-motion and WCAG 2.2 motion criteria.
[[frontend-impl-popover-dialog-anchor]] : popover and dialog entrance / exit using View Transitions.
[[frontend-impl-responsive-layout-fluid]] : fluid clamp() paired with these animations for responsive choreography.