Retune the motion vocabulary: slow, soft, subtle - #397
Merged
Conversation
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.
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The animations read as heavy. Durations were only part of it.
The curve was doing most of the damage
--ease-outwascubic-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/590→220/420/700/820. The curve makes it soft, the durations make it slow. Neither alone was enough.Distances cut, so opacity leads
node-appearexpand-inRevealliftRevealslideAn 8-10% scale jump is a large visual event; at the new longer durations it would have read as sluggish and heavy. The
Revealnumbers 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-pulsewas0 → 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-slowit stopped reading as a pulse and started reading as a failing bulb. Now a single settle..motion-shimmerwas1.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. Now2.4s linear..motion-dash-march3s → 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 staleReact 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-motionCSS rule cannot reach a JS animation.Verification
tsc,eslint, both check scripts andnext buildpass.Confirmed resolving in Chrome: tokens
.22s / .42s / .7s / .82s,--ease-outcubic-bezier(.22, .61, .36, 1), theduration-*andease-oututilities all emit, and a liveRevealon the homepage reports0.7s, the new curve, andtranslate-y-1.5.--dur-faststays the shortest token, since theprefers-reduced-motionblock clamps every animation and transition to it.