Skip to content

ADR-048: CSS-Native Motion System

Accepted (amended 2026-07-05 by ADR-060: the cursor spotlight ships as an Astro atom with a deferred module script rather than a client:idle island, and the showcase hosts two sanctioned demo islands — MotionLab and SignalsCounter — outside this ADR’s “no other component hydrates for motion” budget)

The v2 design language (ADR-047) calls for “restraint, then one bright move” — a near-monochrome dark surface with a small set of deliberate motions. We need a motion system that delivers that polish without violating the starter’s performance and zero-JS philosophy (Constitution rules 2 and 7: client:load forbidden, prefer CSS over JS).

Modern CSS makes most of this achievable with no JavaScript at all: cross-document @view-transition, scroll-driven animation-timeline: view(), registered @property <angle> for animatable gradients, and clip-text sweeps. Only one technique (a pointer-tracking spotlight) genuinely needs JS.

  • Zero-JS baseline: motion must not regress the no-JS rendering path
  • Compositor-only: animations touch transform, opacity, background-position, or a registered <angle> — never layout-triggering properties
  • Accessibility: every animation gated behind prefers-reduced-motion: no-preference with a meaningful resting state
  • Bounded decoration: a hard cap on looping/decorative motion so the page never strobes
  • Token alignment: durations and easings map 1:1 to the existing motion.duration / motion.ease tokens in base.json

Option 1: JS animation library (Framer Motion / GSAP)

Section titled “Option 1: JS animation library (Framer Motion / GSAP)”

Pros: Rich API, easy orchestration

Cons: Ships KBs of JS and forces hydration; violates the zero-JS baseline; main-thread cost; overkill for seven small effects

Option 2: CSS-native, one optional island (chosen)

Section titled “Option 2: CSS-native, one optional island (chosen)”

Description: Six techniques are 100% CSS; one (cursor spotlight) is a ~0.4KB client:idle island that only feeds two CSS variables. All gated behind prefers-reduced-motion.

Pros:

  • No hydration except one tiny opt-in island
  • Compositor-only — no main-thread jank
  • Degrades to sensible static states under reduced motion / no JS

Cons:

  • Some techniques need newer browser support (@view-transition, animation-timeline) — acceptable because each degrades to a static fallback

Pros: Simplest, cheapest

Cons: Forgoes the design language’s single expressive differentiator

Adopt the seven-technique CSS-native system (Option 2).

#TechniqueCostJS
1Page transitions — @view-transition { navigation: auto }Compositornone
2Headline gradient morph — shared view-transition-name: hero-gradCompositornone
3Scroll reveals — animation-timeline: view() (via the existing ScrollReveal component)Compositornone
4Conic glow border — registered @property --glow-angLow paintnone
5Text sheen / CTA shimmer — clip-text + skewed barCompositornone
6Grain overlay — fixed feTurbulence SVG at ~5%1 static layernone
7Cursor spotlight — pointer-tracked radial glowCompositor1 island
  • JS budget: exactly one optional JS surface (cursor spotlight), rAF-throttled, writing only --mx / --my. No other component hydrates for motion. (Amended: shipped as a deferred module <script> in the CursorSpotlight atom — same deferral and no-JS fallback, no hydration runtime. The showcase’s MotionLab island controls a CSS animation and is sanctioned separately by ADR-060.)
  • Decorative-loop cap: one conic glow border per view + two slow sheen loops; all pausable; nothing strobes.
  • Reduced motion: every keyframe block sits inside @media (prefers-reduced-motion: no-preference). With reduced motion on, elements render at their resting state (gradients hold a static position, reveals are fully visible, the spotlight shows a static centered glow, grain is unaffected because it never moves).
  • Token mapping: durations/easings reference --duration-* / --ease-* (from motion.duration / motion.ease); no new motion tokens are introduced.
  • Premium motion with effectively zero JS
  • No layout thrash; all effects compositor-cheap
  • Accessible by contract — gated with real resting states
  • Relies on newer CSS features; older browsers get the static fallback (acceptable)
  • The grain SVG adds ~3KB inline (cached once)
  • The cursor spotlight is opt-in per page; pages that don’t include it pay nothing
  • JS surface: bundle analysis shows no new hydrated component except the single client:idle spotlight island; pnpm perf:budgets stays within the JS budget
  • Reduced motion: toggling OS reduced-motion settles every loop to its resting state
  • No layout jank: animations are limited to compositor properties (manual DevTools check)
  • CSS budget: total CSS (incl. motion) stays < 50KB

The headline gradient morph (technique 2) uses the Variant B (OKLCH longer-hue) sweep selected for this starter.

  • Testable consequences:
    • TC-1: keyframe and animation declarations in src/ touch only compositor-safe properties (transform, opacity, background-position, registered custom properties).
    • TC-2: every animation is gated behind prefers-reduced-motion: no-preference.
  • Checks:
    • TC-1, TC-2 → check motion-gated (status: warn) — flagged medium-brittle at creation; calibrate in warn before any promotion
  • Not machine-checkable: meaningful resting states and the boundedness of decorative motion are judgments.
  • Graduation log: (empty at creation; entries added when a check changes status)

Date: 2026-06-06
Participants: Chris Pezza, template maintainers
Outcome: Accepted