Animation is the easiest way to make a website feel premium — and the easiest way to make it feel annoying. The difference comes down to restraint.
The Rule of Purpose
Every animation needs to answer one question: what does this help the user understand? If the answer is "nothing, it just looks cool," cut it.
Good reasons to animate:
- Showing a state change (loading, success, error)
- Guiding attention to new content
- Creating continuity between views
- Providing feedback on interaction
Bad reasons to animate:
- Because the library supports it
- Because the inspiration site did it
- Because the page feels "empty" without it
Duration Matters More Than Easing
Most web animations are too slow. Here's the timing we follow:
| Interaction | Duration | Why | |-------------|----------|-----| | Button hover | 150ms | Instant feedback, no waiting | | Tooltip appear | 200ms | Quick but not jarring | | Modal open | 250ms | Smooth entrance without delay | | Page transition | 300ms | Enough to feel intentional | | Orchestrated entrance | 400–600ms | Complex sequences, staggered |
Anything longer and users are waiting instead of engaging. The exception: scroll-triggered reveals can be slower (600–800ms) because the user controls the pace.
The Properties That Matter
Always animate (GPU-accelerated, no jank):
transform: translateY(), scale(), rotate()
opacity: 0 → 1
filter: blur()
Avoid animating (triggers layout recalculation):
width, height
margin, padding
top, left, right, bottom
font-size
Use will-change sparingly and only when you've measured the impact. It's a hint, not a magic fix — overuse actually hurts performance.
Our Animation Stack
For most projects, we reach for:
- CSS transitions — for simple hover states and micro-interactions
- Framer Motion — for React-based orchestrated animations
- GSAP — for complex scroll-driven sequences and timeline control
- Lenis — for smooth scroll normalization
We avoid animation libraries that add more than 10KB to the bundle for simple effects. If a CSS transition can do the job, use it.
Real Example: Scroll-Triggered Reveals
The reveal pattern we use across most of our sites:
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
Simple. Lightweight. No library needed. Combined with an Intersection Observer, it handles 90% of scroll-triggered animation needs.
Performance Is a Feature
An animation that causes jank is worse than no animation. Before shipping any animation:
- Test on a mid-range Android device — not your M3 MacBook Pro
- Check the Performance tab — look for long frames (>16ms)
- Verify compositor-only — green indicators in the Layers panel
- Test with reduced motion — respect
prefers-reduced-motionalways
We treat motion as a design material with the same rigor as typography or color. It has rules, it has a budget, and it should always serve the user first.



