Skip to content

Retune the motion vocabulary: slow, soft, subtle - #397

Merged
AnayDhawan merged 1 commit into
mainfrom
motion-retune
Sep 2, 2026
Merged

Retune the motion vocabulary: slow, soft, subtle#397
AnayDhawan merged 1 commit into
mainfrom
motion-retune

Conversation

@AnayDhawan

Copy link
Copy Markdown
Member

The animations read as heavy. Durations were only part of it.

The curve was doing most of the damage

--ease-out was cubic-bezier(0.16, 1, 0.3, 1) — an expo-out. It leaves the start at very high velocity and slams into the end. That shape reads as punchy no matter how long you make it, because the eye reads the acceleration profile, not the elapsed time. Lengthening an expo-out just produces a longer slam.

Now cubic-bezier(0.22, 0.61, 0.36, 1), a quad-out that decelerates evenly from the first frame so nothing lurches.

Durations lengthened on top of that: 170/280/450/590220/420/700/820. The curve makes it soft, the durations make it slow. Neither alone was enough.

Distances cut, so opacity leads

was now
node-appear scale .92 scale .98
expand-in scale .9 + 4px scale .985 + 2px
Reveal lift 12px 6px
Reveal slide 16px 8px

An 8-10% scale jump is a large visual event; at the new longer durations it would have read as sluggish and heavy. The Reveal numbers matter most — they fire section after section down the marketing site, so they set the felt character of the whole thing more than any single interaction does.

Two animations were the wrong shape, not just the wrong speed

highlight-pulse was 0 → 1 → .6 → 1 → .85: a flash, a dip, a second flash. Two direction reversals is the most attention-grabbing shape in the file, and at the longer --dur-slow it stopped reading as a pulse and started reading as a failing bulb. Now a single settle.

.motion-shimmer was 1.6s ease-out infinite. An eased curve on a loop decelerates into the end of each cycle then restarts at full speed, so it visibly stutters once per pass — which is why skeletons read as busy. A loop has no ends to ease against. Now 2.4s linear.

.motion-dash-march 3s → 4.5s. It runs continuously on contradiction edges, so it's the animation most likely to sit in peripheral vision for minutes at a time. A marching dash is a legibility signal, not an alarm.

Shader drift 1.15 → 0.65

This deliberately reverses the 2026-08-23 raise rather than drifting away from it by accident, and says so in the comment. Two things changed since that call: the ramp is high-chroma neon now (#335), and saturated colour makes the same movement read as far more movement; and a hero drifting faster than every transition sitting on top of it was the one thing left fighting the retune.

New lib/motion.ts, because four durations had already gone stale

React Flow's imperative viewport API takes a real number and animates in JS, so it can't read a custom property. Four call sites held 590 / 320 / 300 / 160, hand-matched to the tokens when they were written.

This retune would have left the canvas animating at the old, faster speeds while every CSS transition around it slowed — exactly the drift a token system exists to prevent. They now read from one module, which also writes down the two things that aren't automatic: the values must be kept in sync by hand, and the prefers-reduced-motion CSS rule cannot reach a JS animation.

Verification

tsc, eslint, both check scripts and next build pass.

Confirmed resolving in Chrome: tokens .22s / .42s / .7s / .82s, --ease-out cubic-bezier(.22, .61, .36, 1), the duration-* and ease-out utilities all emit, and a live Reveal on the homepage reports 0.7s, the new curve, and translate-y-1.5.

--dur-fast stays the shortest token, since the prefers-reduced-motion block clamps every animation and transition to it.

The animations read as heavy. The durations were only part of it.

The curve was the main culprit
- --ease-out was cubic-bezier(0.16, 1, 0.3, 1), an expo-out: it leaves
  the start at very high velocity and slams into the end. That shape
  reads as punchy no matter how long it runs, because the eye reads the
  acceleration profile rather than the elapsed time. Lengthening an
  expo-out just produces a longer slam.
- Replaced with a quad-out, cubic-bezier(0.22, 0.61, 0.36, 1), which
  decelerates evenly from the first frame so nothing lurches.

Durations lengthened on top of that
- 170/280/450/590 -> 220/420/700/820. The curve makes it soft, the
  durations make it slow; neither alone was enough.

Distances cut, because motion should be led by opacity
- node-appear scale .92 -> .98
- expand-in scale .9 + 4px -> .985 + 2px
- Reveal lift 12px -> 6px, slide 16px -> 8px. These fire section after
  section down the marketing site, so they set the felt character of the
  whole thing more than any single interaction does.

Two animations were the wrong shape, not just the wrong speed
- highlight-pulse was 0 -> 1 -> .6 -> 1 -> .85, a flash, a dip, a second
  flash. Two direction reversals is the most attention-grabbing shape in
  the file, and at the longer duration it stopped reading as a pulse and
  started reading as a failing bulb. Now a single settle.
- .motion-shimmer was `1.6s ease-out infinite`. An eased curve on a LOOP
  decelerates into the end of each cycle then restarts at full speed, so
  it visibly stutters once per pass. A loop has no ends to ease against.
  Now 2.4s linear.
- .motion-dash-march 3s -> 4.5s. It runs continuously on contradiction
  edges, so it is the animation most likely to sit in peripheral vision
  for minutes.

Shader drift 1.15 -> 0.65
- Deliberately reverses the 2026-08-23 raise rather than drifting from
  it. Two things changed since: the ramp is high-chroma neon now, and
  saturated colour makes the same movement read as much more movement;
  and a hero drifting faster than every transition on top of it was the
  one thing left fighting the retune.

New lib/motion.ts, because four durations had already gone stale
- React Flow's imperative viewport API takes a number and animates in
  JS, so it cannot read a custom property. Those four call sites held
  590/320/300/160, hand-matched to the tokens when they were written.
  The retune would have left the canvas animating at the old speeds
  while everything around it slowed: exactly the drift a token system
  exists to prevent. They now read from one place, with the sync
  requirement and the reduced-motion caveat written down.
@AnayDhawan AnayDhawan added design Visual design, UX, or copy owner-anay Anay: lib, scripts, mcp, api design-system Tokens, primitives, consistency labels Sep 2, 2026
@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
pepiros Ready Ready Preview Sep 2, 2026 7:01pm UTC

@AnayDhawan
AnayDhawan merged commit c0a2bff into main Sep 2, 2026
3 checks passed
@AnayDhawan
AnayDhawan deleted the motion-retune branch September 2, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

design Visual design, UX, or copy design-system Tokens, primitives, consistency owner-anay Anay: lib, scripts, mcp, api

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant