Homepage (site/index.html): integration-v14 promoted, Writings section integrated with 33 pieces clustered by type (stories/essays/miscellany), Writings welcome lightbox, content frame at 98% opacity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.8 KiB
Animation & Microinteractions
Guidelines for when and how to animate UI elements effectively.
The Purpose of Animation
Animation should serve a function, not just look nice.
Valid Reasons to Animate
| Purpose | Example |
|---|---|
| Feedback | Button press confirmation, form submission success |
| Orientation | Showing where something came from or went to |
| Focus | Drawing attention to important changes |
| Teaching | Demonstrating how something works |
| Continuity | Maintaining context during state changes |
| Delight | Occasional surprise (use sparingly) |
Invalid Reasons to Animate
- "It looks cool"
- "The competition does it"
- "To show off our skills"
- Every state change
- To hide slow performance
Timing & Duration
Duration Guidelines
| Animation Type | Duration | Rationale |
|---|---|---|
| Micro-feedback (hover, press) | 100-150ms | Must feel instant |
| Simple transitions (fade, slide) | 150-250ms | Noticeable but quick |
| Complex transitions (modal, navigation) | 250-350ms | Need time to follow |
| Entrances/Reveals | 200-400ms | Can be slightly longer |
| Decorative/Emphasis | 300-500ms | Purpose is to be noticed |
The 200ms Rule
Most UI animations should be around 200ms:
- Faster than 100ms → Too fast to perceive
- Slower than 400ms → Feels sluggish, interrupts flow
Exception: Loading and progress indicators can be slower because they represent real waiting.
Duration by Distance
Longer travel distance = longer duration (but not proportionally).
Small movement (8-16px): 100-150ms
Medium movement (50-100px): 150-250ms
Large movement (full screen): 250-350ms
Easing Functions
Easing makes motion feel natural. Linear motion looks robotic.
Common Easing Curves
| Easing | Use For | Feel |
|---|---|---|
| ease-out | Elements entering | Fast start, gentle stop |
| ease-in | Elements leaving | Gentle start, fast exit |
| ease-in-out | Elements moving within view | Smooth throughout |
| linear | Progress indicators, opacity changes | Mechanical (intentional) |
When to Use Each
Ease-out (default for entrances):
transition: transform 200ms ease-out;
- Modals appearing
- Notifications sliding in
- Dropdowns opening
- Tooltips appearing
Ease-in (for exits):
transition: opacity 150ms ease-in;
- Modals dismissing
- Elements fading out
- Notifications leaving
Ease-in-out (for on-screen movement):
transition: transform 250ms ease-in-out;
- Tab indicators sliding
- Carousel transitions
- Drawer/sidebar toggling
Custom Cubic Bezier
For more personality, customize curves:
/* Snappy entrance */
transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
/* Smooth overshoot */
transition: transform 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
Common Animation Patterns
Button States
Hover:
.btn {
transition: background-color 100ms ease-out, transform 100ms ease-out;
}
.btn:hover {
background-color: var(--btn-hover);
}
Active/Pressed:
.btn:active {
transform: scale(0.97);
}
Loading state:
.btn.loading {
opacity: 0.7;
pointer-events: none;
}
.btn.loading .spinner {
animation: spin 1s linear infinite;
}
Modal Entrance/Exit
Enter:
.modal {
opacity: 0;
transform: scale(0.95) translateY(-10px);
transition: opacity 200ms ease-out, transform 200ms ease-out;
}
.modal.open {
opacity: 1;
transform: scale(1) translateY(0);
}
Exit:
.modal.closing {
opacity: 0;
transform: scale(0.95);
transition: opacity 150ms ease-in, transform 150ms ease-in;
}
Dropdown/Menu
.dropdown {
opacity: 0;
transform: translateY(-8px);
pointer-events: none;
transition: opacity 150ms ease-out, transform 150ms ease-out;
}
.dropdown.open {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
Toast/Notification
.toast {
transform: translateX(100%);
transition: transform 300ms ease-out;
}
.toast.visible {
transform: translateX(0);
}
.toast.exiting {
transform: translateX(100%);
transition: transform 200ms ease-in;
}
Skeleton Loading
.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; }
}
Loading States
Types of Loading Indicators
| Type | Use When | Example |
|---|---|---|
| Spinner | Unknown duration, short wait expected | Button submission |
| Progress bar | Known progress, longer operations | File upload |
| Skeleton | Loading content layout | Feed items |
| Pulse/Shimmer | Refreshing existing content | Pull to refresh |
Spinner Guidelines
- Don't show immediately (wait 300-500ms)
- If wait < 1 second, spinner may not be needed
- Position in context (where content will appear)
- Provide cancel option for long operations
// Delay spinner to avoid flash for fast operations
const [showSpinner, setShowSpinner] = useState(false);
useEffect(() => {
if (isLoading) {
const timer = setTimeout(() => setShowSpinner(true), 400);
return () => clearTimeout(timer);
}
setShowSpinner(false);
}, [isLoading]);
Progress Bar Guidelines
- Show percentage when meaningful
- Don't let it jump backwards
- Consider indeterminate state if progress unknown
- Complete to 100% before hiding
Skeleton Screen Guidelines
- Match layout of actual content
- Use consistent bone shapes
- Animate subtly (shimmer, not bounce)
- Replace with content immediately when loaded
Microinteractions
Small animations that provide feedback and delight.
Effective Microinteractions
Toggle switches:
.toggle-thumb {
transition: transform 150ms ease-out;
}
.toggle.on .toggle-thumb {
transform: translateX(20px);
}
Checkbox check:
.checkmark {
stroke-dasharray: 20;
stroke-dashoffset: 20;
transition: stroke-dashoffset 200ms ease-out;
}
.checkbox.checked .checkmark {
stroke-dashoffset: 0;
}
Like/Heart animation:
.heart {
transform: scale(1);
transition: transform 150ms ease-out;
}
.heart.liked {
animation: pop 300ms ease-out;
}
@keyframes pop {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
Input focus:
.input {
border-color: #ccc;
transition: border-color 150ms ease-out, box-shadow 150ms ease-out;
}
.input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}
When Microinteractions Help
- Confirming user action occurred
- Showing state change clearly
- Making interface feel responsive
- Guiding attention to changes
When to Skip
- Repetitive actions (every keystroke)
- Performance-critical paths
- Accessibility mode (respect reduce-motion)
Accessibility Considerations
Respect User Preferences
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Provide Alternatives
- Don't rely on animation alone to convey information
- Ensure state changes are visible without animation
- Allow users to disable animations in app settings
Avoid Problematic Animations
| Avoid | Reason |
|---|---|
| Flashing/strobing | Can trigger seizures |
| Parallax scrolling | Causes motion sickness |
| Auto-playing video | Distracting, accessibility |
| Infinite loops | Drains attention, battery |
Performance Guidelines
GPU-Accelerated Properties
Animate these for smooth 60fps:
transform(translate, scale, rotate)opacity
Avoid animating (causes reflow/repaint):
width,heighttop,left,right,bottommargin,paddingborder-widthfont-size
Use will-change Sparingly
/* Only for elements about to animate */
.modal {
will-change: transform, opacity;
}
/* Remove after animation */
.modal.static {
will-change: auto;
}
Batch Animations
Start animations together, not staggered excessively:
- 0-50ms stagger: feels cohesive
- 100ms+ stagger: feels slow, sequential
Test on Low-end Devices
What's smooth on your MacBook may stutter on a budget Android phone. Test on real devices or throttle CPU in DevTools.
Animation Checklist
Before shipping an animation:
- Does it serve a purpose (not just decoration)?
- Is duration appropriate for the action?
- Does easing feel natural?
- Does it work with
prefers-reduced-motion? - Is it GPU-accelerated (transform/opacity)?
- Does it perform well on low-end devices?
- Can the interface function without it?