Motion serves three purposes. If it doesn't do one of these — remove it:
- Feedback — "It worked" (button press, toggle, submit)
- Continuity — "Where it went" (page transition, drawer)
- Hierarchy — "Look here" (attention, notification)
| Category | Duration | Examples |
|---|---|---|
| Instant | 90–150ms | Hover, press, toggle, checkbox |
| State change | 160–240ms | Accordion, tab switch, dropdown |
| Large transition | 240–360ms | Modal, drawer, page transition |
| Complex | 360–500ms | Skeleton → content, multi-step |
Rule: Nothing over 500ms in product UI. Users will notice and feel sluggish.
| Action | Easing | CSS |
|---|---|---|
| Element entering | ease-out | cubic-bezier(0, 0, 0.2, 1) |
| Element exiting | ease-in | cubic-bezier(0.4, 0, 1, 1) |
| State change | ease-in-out | cubic-bezier(0.4, 0, 0.2, 1) |
Never use: linear for UI animations. It feels mechanical and robotic.
.button:active {
transform: scale(0.98);
transition: transform 90ms ease-out;
}.item:hover {
background-color: var(--neutral-100);
transition: background-color 120ms ease;
}.toggle-knob {
transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
}.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}When multiple items enter, stagger by 50-80ms:
.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 60ms; }
.item:nth-child(3) { animation-delay: 120ms; }Required. Always respect user preferences:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}- ❌ Animation > 500ms on any interactive element
- ❌ Linear easing on UI elements
- ❌ Everything animating simultaneously (stagger instead)
- ❌ Decorative animations that serve no feedback/continuity/hierarchy purpose
- ❌ Animations that block user interaction