Motion earns its place when it explains something: where a panel came from, what just changed, what to look at next. When it only decorates, it is a tax on every visit.
We treat animation like any other resource with a budget, and we delete the ones that do not pay for themselves.
#Set a budget
Our rule of thumb is that interface transitions stay under 300ms and nothing blocks input. Larger moments, like a hero reveal, run once per session and never delay content.
We animate transform and opacity only. Anything that triggers layout gets rewritten or removed, because it is the first thing to jank on a mid-range phone.
- Under 300ms for interface feedback
- Transform and opacity only, no layout properties
- Every animation has a reduced-motion alternative
#Respect reduced motion
Reduced motion does not mean no motion. We swap travel for a short fade so state changes stay legible without anything sliding across the screen.
When we cannot explain what an animation communicates in one sentence, it goes. Deleting is the most underrated animation technique.
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches
export function reveal(el: HTMLElement) {
return el.animate(
reduce
? [{ opacity: 0 }, { opacity: 1 }]
: [
{ opacity: 0, transform: 'translateY(16px)' },
{ opacity: 1, transform: 'translateY(0)' },
],
{ duration: reduce ? 120 : 280, easing: 'cubic-bezier(0.2, 0, 0, 1)', fill: 'both' },
)
}



