SuperAPI AI Directory

frontend-impl-view-transitions-scroll-animations

Frontend & websites

View repository

Updated 10-15 22:18

About this skill

Frontend Impl View Transitions + Scroll-Driven Animations

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).

Sources : MDN: View Transition API (verified 2026-05-19), MDN: animation-timeline (verified 2026-05-19), MDN: scroll-timeline (verified 2026-05-19), MDN: view-timeline (verified 2026-05-19), MDN: CSS scroll snap (verified 2026-05-19).

Quick Reference

View Transition API : same-document (SPA)

async function navigate(url) {
  if (!document.startViewTransition || prefersReducedMotion()) {
    updateDOM(url);
    return;
  }
  const transition = document.startViewTransition(() => updateDOM(url));
  await transition.finished;
}

document.startViewTransition(callback) :

Aspect Detail
Callback Synchronously update the DOM inside. The browser snapshots BEFORE the call and again AFTER.
Return value ViewTransition object.
.ready Promise resolving when pseudo-elements are mounted and the transition is ready to animate.
.finished Promise resolving when the transition completes.
.updateCallbackDone Promise resolving when the DOM update callback completes.
.skipTransition() Cancel the animation; jump to the final state.
Pseudo-element tree ::view-transition -> ::view-transition-group(name) -> ::view-transition-image-pair(name) -> ::view-transition-old(name) / ::view-transition-new(name).

View Transition : cross-document (MPA)

Per MDN: View Transition API (verified 2026-05-19), declare on BOTH source and destination :

@view-transition { navigation: auto; }

CRITICAL : if declared only on the source OR only on the destination, the transition silently no-ops.

view-transition-name and pseudo-element animation

.product-card[data-id="42"] .image { view-transition-name: product-42-image; }

::view-transition-old(product-42-image),
::view-transition-new(product-42-image) {
  animation-duration: 300ms;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

Rules :

  • 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.

Scroll-driven animations : animation-timeline values

Per MDN: animation-timeline (verified 2026-05-19). Limited Availability in 2026 ; gate with @supports.

Value Meaning
auto (default) Time-based ; standard DocumentTimeline.
none No animation.
scroll(<axis>? <scroller>?) Anonymous SCROLL progress timeline. Axes : block (default) / inline / x / y. Scrollers : nearest (default) / root / self.
view(<axis>? <inset>?) 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.

.gallery { display: grid; grid-auto-flow: column; overflow-x: scroll; scroll-snap-type: x mandatory; }
.gallery > .slide { scroll-snap-align: start; scroll-snap-stop: always; }
Property On Values
scroll-snap-type scroll container 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();
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) {
    animation: none;
  }
  .card { animation: none; }
  .progress { animation: none; }
  .gallery { scroll-snap-type: none; }
}

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

<a href="/products/42" class="product-card" data-id="42">
  <img class="image" src="/img/42.jpg" alt="">
  <h3>Product 42</h3>
</a>
.product-card[data-id="42"] .image { view-transition-name: product-42-image; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(*), ::view-transition-new(*) { animation: none; }
}
document.querySelectorAll('.product-card').forEach(a => {
  a.addEventListener('click', (e) => {
    e.preventDefault();
    const url = a.href;
    if (!document.startViewTransition || matchMedia('(prefers-reduced-motion: reduce)').matches) {
      location.assign(url);
      return;
    }
    document.startViewTransition(() => loadDetailFor(url));
  });
});

The image morphs from list to detail because both contain an .image with the same view-transition-name.

Pattern 2 : cross-document MPA transition

In /list.html and /detail.html (both ship the same CSS) :

@view-transition { navigation: auto; }

.hero-image { view-transition-name: hero; }

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.

Pattern 3 : scroll progress bar

.progress {
  position: fixed; inset-block-start: 0; inset-inline: 0; block-size: 4px;
  background: oklch(0.6 0.2 250);
  transform-origin: left; transform: scaleX(0);
  animation: grow auto linear;
  animation-timeline: scroll(block root);
}
@keyframes grow { to { transform: scaleX(1); } }
@media (prefers-reduced-motion: reduce) {
  .progress { animation: none; transform: none; }
}

A pure-CSS scroll progress indicator that costs no JavaScript.

Pattern 4 : reveal-on-scroll using view()

.card {
  opacity: 0;
  transform: translateY(2rem);
  animation: reveal auto linear;
  animation-timeline: view();
  animation-range: entry 20% cover 30%;
}
@keyframes reveal {
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .card { opacity: 1; transform: none; animation: none; }
}

animation-range: entry 20% cover 30% controls when in the element's view-progress the animation runs.

Pattern 5 : horizontal scroll-snap carousel

.carousel { display: grid; grid-auto-flow: column; gap: 1rem; overflow-x: scroll; scroll-snap-type: x mandatory; padding-inline: 2rem; }
.carousel > .slide { scroll-snap-align: center; scroll-snap-stop: always; inline-size: 80%; aspect-ratio: 16 / 9; }
@media (prefers-reduced-motion: reduce) { .carousel { scroll-snap-type: x proximity; } }

mandatory ensures slides snap into place ; on reduced-motion the rule downgrades to proximity so the user has finer control.

Pattern 6 : @supports gate for Limited Availability

.fade-in { opacity: 1; }   /* default : no animation */

@supports (animation-timeline: scroll()) {
  .fade-in {
    opacity: 0;
    animation: reveal auto linear;
    animation-timeline: view();
  }
  @keyframes reveal { to { opacity: 1; } }
}

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.