Motion tokens
Five durations · six easingsCompose every animation from these. The duration says how long, the easing says how it feels. Hit play to watch each one run.
Durations
Easings
Four tiers of motion
Organize by what triggers itPick a tier by intent. Mechanical feel for structure; a spring is reserved for one or two personality moments per page.
Cursor
Polish you do not notice. Hover lift, button color shift, link underline, a live status dot.
Scroll-in
Content arrives, fires once on view. Fade-up, counter, bar-fill, staggered deal-in.
Scroll position
A scene unfolds as you scroll. Scrub, or pin and autoplay on entry.
Plays on its own
Depicts what the product does. Typewriter, SVG line-draw, the self-demo.
The pattern library
Live · replay each · copy the codeNine building blocks. Hit Replay to run any one again, hover the cursor ones, and copy the exact code straight off the page.
Scroll fade-up
The default entrance for sections, cards, and major blocks. Subtle, mo-4, ease-out. The page-wide requirement.
IntersectionObserver · opacity + transform
.fade-up{opacity:0;transform:translateY(16px);
transition:opacity var(--mo-4) var(--ease-out),
transform var(--mo-4) var(--ease-out)}
.fade-up.is-in{opacity:1;transform:none}Card hover lift
Every card that links somewhere lifts on hover. Nothing interactive looks inert. Keep the lift at 3 to 4px so the repaint stays small.
Hover the card · transform + shadow
.card{transition:transform var(--mo-3) var(--ease-out),
box-shadow var(--mo-3) var(--ease-out)}
.card:hover{transform:translateY(-3px);
box-shadow:0 6px 22px rgba(11,22,36,.13)}Button color shift
Hover moves to the matching secondary token: rust to rust-2, gold to gold-2, navy to navy-2. Never invent a hover color. Under 200ms so it feels editorial.
Hover the button · background only
.btn-rust{background:#c24c32;color:#fffaf0;
transition:background var(--mo-1) var(--ease-warm)}
.btn-rust:hover{background:#a33e28}Hero typewriter
For hero headlines where the rotating words are real content: product names, market names, reader towns. Reserve the width or the headline reflows.
Live · real content only
const words=['break','house','media','lab'];
let w=0,c=0,del=false;(function tick(){
const word=words[w]; c+=del?-1:1;
tw.textContent=word.slice(0,c); let wait=del?45:95;
if(!del&&c===word.length){del=true;wait=1500}
else if(del&&c===0){del=false;w=(w+1)%words.length;wait=320}
setTimeout(tick,wait)})();Animated counter
Stat cards count up once on view. Use tabular figures (JetBrains Mono is) so digits do not shift width and cause reflow.
Scroll-in · requestAnimationFrame
const end=+el.dataset.target,t0=performance.now();
(function step(t){const p=Math.min((t-t0)/900,1);
const e=1-Math.pow(1-p,3);
el.textContent=fmt.format(Math.round(end*e));
if(p<1)requestAnimationFrame(step)})(t0);SVG line draw
Hero diagrams, route maps, system sketches. Strong fit for a right-side hero animation that depicts what the product does.
Scroll-in · stroke-dashoffset
.draw{stroke-dasharray:1;stroke-dashoffset:1}
.draw.is-in{animation:lmv-draw 1.6s var(--ease-rule) forwards}
@keyframes lmv-draw{to{stroke-dashoffset:0}}Staggered deal-in
A row of cards deals in one after another, 80ms apart. Use for a grid that should feel assembled, not dumped.
Scroll-in · animation-delay per child
.row.go span{opacity:0;transform:translateY(14px);
animation:dfade var(--mo-3) var(--ease-out) forwards}
.row.go span:nth-child(2){animation-delay:.08s}
.row.go span:nth-child(3){animation-delay:.16s}Bar fill
Ranking bars grow from zero on view. The mo-5 duration makes a chart feel deliberate. Pair it with the counter for a stats strip.
Scroll-in · width 0 to target
.bf{width:0}
.bars.go .bf{animation:barfill var(--mo-5) var(--ease-warm) forwards}
@keyframes barfill{to{width:var(--w)}}Live status pulse
A single dot ring-pulses to mark a live or running state. The only infinite animation allowed, and only on a genuine status.
Ambient · ring expand + fade
.dot{animation:pulse 2s infinite}
@keyframes pulse{
0%{box-shadow:0 0 0 0 rgba(63,122,90,.5)}
70%{box-shadow:0 0 0 9px rgba(63,122,90,0)}}Principles
The rules under every patternCompositor only
Animate opacity and transform, never layout properties. The repaint stays off the main thread and Lighthouse never moves.
IntersectionObserver, not scroll
Entrances fire from an observer that unobserves after one run. Scroll-event listeners are banned outright.
Reduced-motion is real
One global rule kills every animation for users who ask. Paste it once per site; the static state must still read correctly.
@media (prefers-reduced-motion: reduce){
*,*::before,*::after{
animation:none !important; transition:none !important}}Banned motion
What never shipsMotion that calls attention to itself
If it does not have a job, it does not ship. These are out, system-wide.
- Infinite spinning loaders as decoration
- Bouncing elements
- Autoplay video as a hero substitute
- Anything that blocks reading or delays content
- Scroll-event listeners for entrances
- A spring on everything (one or two moments per page)