Animation Principles for UI
Disney's 12 animation principles adapted for web and mobile interfaces. Based on dylantarre/animation-principles.
The 12 Principles: Web Implementation
| # |
Principle |
Web Implementation |
When to Use |
| 1 |
Squash & Stretch |
transform: scale() on interaction states |
Button press, toggle bounce, badge update |
| 2 |
Anticipation |
Slight reverse movement before action |
Dropdown opens, drag start, navigation |
| 3 |
Staging |
Focus attention via motion hierarchy |
Modal focus, form field activation |
| 4 |
Straight Ahead / Pose to Pose |
JS frame-by-frame vs CSS keyframes |
Progress (straight) vs state snaps (pose) |
| 5 |
Follow Through / Overlapping |
Staggered child animations, elastic easing |
Menu items, list entries, content settle |
| 6 |
Slow In / Slow Out |
ease-in-out, cubic-bezier curves |
Every UI transition (never use linear) |
| 7 |
Arc |
motion-path or bezier translate transforms |
Toggle switches, circular menus |
| 8 |
Secondary Action |
Shadows, glows responding to primary motion |
Button shadow on hover, icon color shift |
| 9 |
Timing |
Duration ranges per interaction type |
See timing table below |
| 10 |
Exaggeration |
Scale beyond 1.0, overshoot animations |
Emphasis moments, error shakes |
| 11 |
Solid Drawing |
Consistent transform-origin, 3D perspective |
All transforms need correct origin |
| 12 |
Appeal |
Smooth 60fps, purposeful motion |
Every animation must have a reason |
Principle Details
Squash & Stretch: Apply scaleY compression on button press, scaleX stretch on hover. Keep volume constant: if you compress Y, expand X slightly. Keep subtle in UI; this is interfaces, not cartoons.
Anticipation: Before expanding a dropdown, shrink it 2-3% first. Before sliding content left, move it 5px right. Hover states prepare for click. Draggable items elevate on grab start.
Staging: Dim background elements during modal focus. Use motion to direct eye flow: animate important elements first. Active form fields are clearly distinguished. One interaction feedback at a time.
Straight Ahead vs Pose to Pose: Use CSS @keyframes for predictable, repeatable animations (pose to pose). Use JavaScript/GSAP for dynamic, physics-based motion (straight ahead). Progress indicators = straight ahead. Checkboxes = pose to pose.
Follow Through & Overlapping: Child elements complete movement after parent stops. Use animation-delay with decreasing values for natural stagger (30-50ms per item). Ripple effects expand past tap point. Toggle switches overshoot then settle.
Slow In / Slow Out: Never use linear for UI motion. Standard: cubic-bezier(0.4, 0, 0.2, 1). Enter: cubic-bezier(0, 0, 0.2, 1) (ease-out). Exit: cubic-bezier(0.4, 0, 1, 1) (ease-in). Exit animations shorter than enter (~60-70% of enter duration).
Arc: Elements in nature move in arcs, not straight lines. Use offset-path or combine X/Y transforms with different easings. Toggle switches travel in slight arc. Circular action buttons expand radially.
Secondary Action: Button shadow grows/blurs on hover. Icon inside button rotates while button scales. Background particles respond to primary element. Shadow responds to elevation changes.
Timing: See the complete timing reference table below. Micro-interactions: 100-200ms. Standard transitions: 200-400ms. Complex sequences: 400-700ms.
Exaggeration: Hover states scale to 1.05-1.1, not 1.01. Error shakes move 10-20px, not 2px. Make motion noticeable but not jarring. Save theatrics for key moments (onboarding, achievement).
Solid Drawing: Maintain consistent transform-origin. Use perspective for 3D depth. Avoid conflicting transforms. Buttons scale from center, tooltips from pointer.
Appeal: Target 60fps using transform and opacity only. Add personality through custom easing curves. Motion should feel intentional: if you can't explain why something animates, remove it.
Timing Reference
By Interaction Type
| Interaction |
Duration |
Easing |
| Hover |
100ms |
ease-out |
| Click/tap feedback |
100ms |
ease-out |
| Toggle switch |
150-200ms |
spring/elastic |
| Checkbox |
150ms |
ease-out |
| Focus ring |
100ms |
ease-out |
| Tooltip show |
150ms |
ease-out |
| Tooltip hide |
100ms |
ease-in |
| Badge update |
200ms |
elastic |
| Form error |
200ms |
ease-out |
| Dropdown open |
200-300ms |
ease-out |
| Modal open |
250-350ms |
ease-out |
| Modal close |
200-300ms |
ease-in |
| Page slide forward |
300-400ms |
ease-out |
| Page slide back |
250-350ms |
ease-out |
| Tab switch |
150-200ms |
ease-out |
| Shared element |
300-400ms |
ease-in-out |
By Time Scale
| Scale |
Duration |
Use For |
Easing |
| Instant |
0-100ms |
Hover, focus, tap feedback, ripples |
ease-out or step |
| Micro |
100-200ms |
Toggles, checkboxes, tooltips, badges |
ease-out, cubic-bezier(0.2, 0, 0, 1) |
| Small |
200-300ms |
Dropdowns, accordions, slides, fades |
ease-out, spring |
| Medium |
300-500ms |
Page transitions, modals, drawers |
ease-in-out, cubic-bezier(0.4, 0, 0.2, 1) |
| Large |
500-800ms |
Complex sequences, onboarding reveals |
cubic-bezier(0.16, 1, 0.3, 1) |
Code Patterns
Micro-interactions
/* Button with squash/stretch */
.button {
transition: transform 100ms ease-out, box-shadow 100ms ease-out;
}
.button:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.button:active {
transform: translateY(0) scale(0.98);
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
/* Toggle switch with overshoot */
.toggle-thumb {
transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.toggle-thumb.active {
transform: translateX(20px);
}
/* Checkbox draw effect */
.checkmark {
stroke-dasharray: 20;
stroke-dashoffset: 20;
transition: stroke-dashoffset 200ms ease-out 50ms;
}
.checkbox:checked + .checkmark {
stroke-dashoffset: 0;
}
/* Dropdown with anticipation */
.dropdown-enter {
animation: dropdown-open 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes dropdown-open {
0% { transform: scaleY(0.98); opacity: 0; }
100% { transform: scaleY(1); opacity: 1; }
}
Page Transitions
/* Crossfade (default) */
.page-exit-active {
opacity: 0;
transition: opacity 200ms ease-in;
}
.page-enter {
opacity: 0;
}
.page-enter-active {
opacity: 1;
transition: opacity 200ms ease-out;
}
/* Slide (hierarchical navigation) */
.page-enter {
transform: translateX(100%);
}
.page-enter-active {
transform: translateX(0);
transition: transform 300ms ease-out;
}
.page-exit-active {
transform: translateX(-30%);
transition: transform 300ms ease-in;
}
/* Shared element (View Transitions API) */
.hero-image {
view-transition-name: hero;
}
::view-transition-old(hero),
::view-transition-new(hero) {
animation-duration: 300ms;
}
Stagger Pattern
/* List items entering with stagger */
.list-item {
opacity: 0;
transform: translateY(10px);
animation: stagger-in 300ms ease-out forwards;
}
.list-item:nth-child(1) { animation-delay: 0ms; }
.list-item:nth-child(2) { animation-delay: 40ms; }
.list-item:nth-child(3) { animation-delay: 80ms; }
.list-item:nth-child(4) { animation-delay: 120ms; }
@keyframes stagger-in {
to { opacity: 1; transform: translateY(0); }
}
Framer Motion (React)
/* Spring-based animation */
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ type: "spring", stiffness: 300, damping: 24 }}
/>
/* Stagger children */
<motion.ul variants={{
show: { transition: { staggerChildren: 0.05 } }
}}>
<motion.li variants={{
hidden: { opacity: 0, y: 10 },
show: { opacity: 1, y: 0 }
}} />
</motion.ul>
/* Layout animation (shared element) */
<motion.div layoutId="hero-card" />
/* Reduced motion */
const prefersReducedMotion = useReducedMotion();
<motion.div
animate={{ x: 100 }}
transition={prefersReducedMotion ? { duration: 0 } : { type: "spring" }}
/>
Easing Reference
| Name |
CSS Value |
Use |
| Standard |
cubic-bezier(0.4, 0, 0.2, 1) |
General purpose |
| Enter (decelerate) |
cubic-bezier(0, 0, 0.2, 1) |
Elements appearing |
| Exit (accelerate) |
cubic-bezier(0.4, 0, 1, 1) |
Elements leaving |
| Sharp |
cubic-bezier(0.4, 0, 0.6, 1) |
Quick, snappy |
| Overshoot |
cubic-bezier(0.34, 1.56, 0.64, 1) |
Playful, bouncy |
| Smooth out |
cubic-bezier(0.16, 1, 0.3, 1) |
Elegant settle |
| Spring (CSS approx) |
cubic-bezier(0.175, 0.885, 0.32, 1.275) |
Natural feel |
Navigation Direction Model
| Pattern |
Transition |
Direction |
| Drill-down (list to detail) |
Slide left / shared element |
Right = forward |
| Tab bar |
Fade / horizontal slide |
Follows tab position |
| Bottom sheet |
Slide up from bottom |
Vertical |
| Modal |
Scale + fade from trigger |
Z-axis |
| Back button |
Reverse of forward |
Left = back |
| Forward navigation |
Animate left/up |
Deeper into hierarchy |
| Backward navigation |
Animate right/down |
Toward home |
Accessibility: Reduced Motion
CSS Implementation
/* Default: full animation */
.element {
transition: transform 300ms ease-out;
}
/* Reduced motion: replace with opacity or instant */
@media (prefers-reduced-motion: reduce) {
.element {
transition: opacity 200ms ease-out;
/* Or: transition: none; */
}
/* Kill all animations globally */
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Safe vs Harmful Motion
| Safe |
Potentially Harmful |
Always Avoid |
| Opacity fades |
Parallax scrolling |
Auto-playing video backgrounds |
| Color transitions |
Background movement |
Infinite looping animations |
| Small scale (<5%) |
Zoom animations |
Flashing (seizure risk) |
| Very slow movement (500ms+) |
Spinning/rotating |
Rapid zoom in/out |
| Non-repeating animations |
Fast repeated animations |
>3 flashes per second |
|
Large moving areas (>1/4 viewport) |
Vestibular-triggering patterns |
WCAG Compliance
- 2.3.3: Motion from interactions can be disabled unless essential
- 2.2.2: Moving content >5 seconds must be pausable
- 2.3.1: No content flashes >3 times per second
Performance Rules
- Animate only
transform and opacity for GPU acceleration
- Use
will-change sparingly; remove after animation completes
- Prefer CSS over JavaScript when animation is predictable
- Keep per-frame work under ~16ms for 60fps
- Test on low-powered devices
- Debounce scroll/resize-triggered animations
- Use
requestAnimationFrame for JS animations
- Virtualize animated lists with 50+ items
Pre-Delivery Checklist
1---2name: animation-principles3description: Disney's 12 animation principles applied to web and mobile UI. Use when implementing CSS animations, JS transitions, micro-interactions, page transitions, loading states, hover effects, or motion design. Covers timing, easing, accessibility (prefers-reduced-motion), and framework patterns (Framer Motion, CSS native, GSAP). Triggers: animate, transition, motion, hover effect, loading animation, page transition, micro-interaction, easing, spring physics.4---56# Animation Principles for UI78Disney's 12 animation principles adapted for web and mobile interfaces. Based on [dylantarre/animation-principles](https://github.com/dylantarre/animation-principles).910## The 12 Principles: Web Implementation1112| # | Principle | Web Implementation | When to Use |13|---|-----------|-------------------|-------------|14| 1 | **Squash & Stretch** | `transform: scale()` on interaction states | Button press, toggle bounce, badge update |15| 2 | **Anticipation** | Slight reverse movement before action | Dropdown opens, drag start, navigation |16| 3 | **Staging** | Focus attention via motion hierarchy | Modal focus, form field activation |17| 4 | **Straight Ahead / Pose to Pose** | JS frame-by-frame vs CSS keyframes | Progress (straight) vs state snaps (pose) |18| 5 | **Follow Through / Overlapping** | Staggered child animations, elastic easing | Menu items, list entries, content settle |19| 6 | **Slow In / Slow Out** | `ease-in-out`, cubic-bezier curves | Every UI transition (never use `linear`) |20| 7 | **Arc** | `motion-path` or bezier translate transforms | Toggle switches, circular menus |21| 8 | **Secondary Action** | Shadows, glows responding to primary motion | Button shadow on hover, icon color shift |22| 9 | **Timing** | Duration ranges per interaction type | See timing table below |23| 10 | **Exaggeration** | Scale beyond 1.0, overshoot animations | Emphasis moments, error shakes |24| 11 | **Solid Drawing** | Consistent transform-origin, 3D perspective | All transforms need correct origin |25| 12 | **Appeal** | Smooth 60fps, purposeful motion | Every animation must have a reason |2627## Principle Details2829**Squash & Stretch**: Apply `scaleY` compression on button press, `scaleX` stretch on hover. Keep volume constant: if you compress Y, expand X slightly. Keep subtle in UI; this is interfaces, not cartoons.3031**Anticipation**: Before expanding a dropdown, shrink it 2-3% first. Before sliding content left, move it 5px right. Hover states prepare for click. Draggable items elevate on grab start.3233**Staging**: Dim background elements during modal focus. Use motion to direct eye flow: animate important elements first. Active form fields are clearly distinguished. One interaction feedback at a time.3435**Straight Ahead vs Pose to Pose**: Use CSS `@keyframes` for predictable, repeatable animations (pose to pose). Use JavaScript/GSAP for dynamic, physics-based motion (straight ahead). Progress indicators = straight ahead. Checkboxes = pose to pose.3637**Follow Through & Overlapping**: Child elements complete movement after parent stops. Use `animation-delay` with decreasing values for natural stagger (30-50ms per item). Ripple effects expand past tap point. Toggle switches overshoot then settle.3839**Slow In / Slow Out**: Never use `linear` for UI motion. Standard: `cubic-bezier(0.4, 0, 0.2, 1)`. Enter: `cubic-bezier(0, 0, 0.2, 1)` (ease-out). Exit: `cubic-bezier(0.4, 0, 1, 1)` (ease-in). Exit animations shorter than enter (~60-70% of enter duration).4041**Arc**: Elements in nature move in arcs, not straight lines. Use `offset-path` or combine X/Y transforms with different easings. Toggle switches travel in slight arc. Circular action buttons expand radially.4243**Secondary Action**: Button shadow grows/blurs on hover. Icon inside button rotates while button scales. Background particles respond to primary element. Shadow responds to elevation changes.4445**Timing**: See the complete timing reference table below. Micro-interactions: 100-200ms. Standard transitions: 200-400ms. Complex sequences: 400-700ms.4647**Exaggeration**: Hover states scale to 1.05-1.1, not 1.01. Error shakes move 10-20px, not 2px. Make motion noticeable but not jarring. Save theatrics for key moments (onboarding, achievement).4849**Solid Drawing**: Maintain consistent `transform-origin`. Use `perspective` for 3D depth. Avoid conflicting transforms. Buttons scale from center, tooltips from pointer.5051**Appeal**: Target 60fps using `transform` and `opacity` only. Add personality through custom easing curves. Motion should feel intentional: if you can't explain why something animates, remove it.5253## Timing Reference5455### By Interaction Type5657| Interaction | Duration | Easing |58|-------------|----------|--------|59| Hover | 100ms | ease-out |60| Click/tap feedback | 100ms | ease-out |61| Toggle switch | 150-200ms | spring/elastic |62| Checkbox | 150ms | ease-out |63| Focus ring | 100ms | ease-out |64| Tooltip show | 150ms | ease-out |65| Tooltip hide | 100ms | ease-in |66| Badge update | 200ms | elastic |67| Form error | 200ms | ease-out |68| Dropdown open | 200-300ms | ease-out |69| Modal open | 250-350ms | ease-out |70| Modal close | 200-300ms | ease-in |71| Page slide forward | 300-400ms | ease-out |72| Page slide back | 250-350ms | ease-out |73| Tab switch | 150-200ms | ease-out |74| Shared element | 300-400ms | ease-in-out |7576### By Time Scale7778| Scale | Duration | Use For | Easing |79|-------|----------|---------|--------|80| Instant | 0-100ms | Hover, focus, tap feedback, ripples | `ease-out` or step |81| Micro | 100-200ms | Toggles, checkboxes, tooltips, badges | `ease-out`, `cubic-bezier(0.2, 0, 0, 1)` |82| Small | 200-300ms | Dropdowns, accordions, slides, fades | `ease-out`, spring |83| Medium | 300-500ms | Page transitions, modals, drawers | `ease-in-out`, `cubic-bezier(0.4, 0, 0.2, 1)` |84| Large | 500-800ms | Complex sequences, onboarding reveals | `cubic-bezier(0.16, 1, 0.3, 1)` |8586## Code Patterns8788### Micro-interactions8990```css91/* Button with squash/stretch */92.button {93 transition: transform 100ms ease-out, box-shadow 100ms ease-out;94}95.button:hover {96 transform: translateY(-1px);97 box-shadow: 0 2px 8px rgba(0,0,0,0.15);98}99.button:active {100 transform: translateY(0) scale(0.98);101 box-shadow: 0 1px 2px rgba(0,0,0,0.1);102}103104/* Toggle switch with overshoot */105.toggle-thumb {106 transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);107}108.toggle-thumb.active {109 transform: translateX(20px);110}111112/* Checkbox draw effect */113.checkmark {114 stroke-dasharray: 20;115 stroke-dashoffset: 20;116 transition: stroke-dashoffset 200ms ease-out 50ms;117}118.checkbox:checked + .checkmark {119 stroke-dashoffset: 0;120}121122/* Dropdown with anticipation */123.dropdown-enter {124 animation: dropdown-open 300ms cubic-bezier(0.34, 1.56, 0.64, 1);125}126@keyframes dropdown-open {127 0% { transform: scaleY(0.98); opacity: 0; }128 100% { transform: scaleY(1); opacity: 1; }129}130```131132### Page Transitions133134```css135/* Crossfade (default) */136.page-exit-active {137 opacity: 0;138 transition: opacity 200ms ease-in;139}140.page-enter {141 opacity: 0;142}143.page-enter-active {144 opacity: 1;145 transition: opacity 200ms ease-out;146}147148/* Slide (hierarchical navigation) */149.page-enter {150 transform: translateX(100%);151}152.page-enter-active {153 transform: translateX(0);154 transition: transform 300ms ease-out;155}156.page-exit-active {157 transform: translateX(-30%);158 transition: transform 300ms ease-in;159}160161/* Shared element (View Transitions API) */162.hero-image {163 view-transition-name: hero;164}165::view-transition-old(hero),166::view-transition-new(hero) {167 animation-duration: 300ms;168}169```170171### Stagger Pattern172173```css174/* List items entering with stagger */175.list-item {176 opacity: 0;177 transform: translateY(10px);178 animation: stagger-in 300ms ease-out forwards;179}180.list-item:nth-child(1) { animation-delay: 0ms; }181.list-item:nth-child(2) { animation-delay: 40ms; }182.list-item:nth-child(3) { animation-delay: 80ms; }183.list-item:nth-child(4) { animation-delay: 120ms; }184185@keyframes stagger-in {186 to { opacity: 1; transform: translateY(0); }187}188```189190### Framer Motion (React)191192```tsx193/* Spring-based animation */194<motion.div195 initial={{ opacity: 0, y: 20 }}196 animate={{ opacity: 1, y: 0 }}197 exit={{ opacity: 0, y: -10 }}198 transition={{ type: "spring", stiffness: 300, damping: 24 }}199/>200201/* Stagger children */202<motion.ul variants={{203 show: { transition: { staggerChildren: 0.05 } }204}}>205 <motion.li variants={{206 hidden: { opacity: 0, y: 10 },207 show: { opacity: 1, y: 0 }208 }} />209</motion.ul>210211/* Layout animation (shared element) */212<motion.div layoutId="hero-card" />213214/* Reduced motion */215const prefersReducedMotion = useReducedMotion();216<motion.div217 animate={{ x: 100 }}218 transition={prefersReducedMotion ? { duration: 0 } : { type: "spring" }}219/>220```221222## Easing Reference223224| Name | CSS Value | Use |225|------|-----------|-----|226| Standard | `cubic-bezier(0.4, 0, 0.2, 1)` | General purpose |227| Enter (decelerate) | `cubic-bezier(0, 0, 0.2, 1)` | Elements appearing |228| Exit (accelerate) | `cubic-bezier(0.4, 0, 1, 1)` | Elements leaving |229| Sharp | `cubic-bezier(0.4, 0, 0.6, 1)` | Quick, snappy |230| Overshoot | `cubic-bezier(0.34, 1.56, 0.64, 1)` | Playful, bouncy |231| Smooth out | `cubic-bezier(0.16, 1, 0.3, 1)` | Elegant settle |232| Spring (CSS approx) | `cubic-bezier(0.175, 0.885, 0.32, 1.275)` | Natural feel |233234## Navigation Direction Model235236| Pattern | Transition | Direction |237|---------|------------|-----------|238| Drill-down (list to detail) | Slide left / shared element | Right = forward |239| Tab bar | Fade / horizontal slide | Follows tab position |240| Bottom sheet | Slide up from bottom | Vertical |241| Modal | Scale + fade from trigger | Z-axis |242| Back button | Reverse of forward | Left = back |243| Forward navigation | Animate left/up | Deeper into hierarchy |244| Backward navigation | Animate right/down | Toward home |245246## Accessibility: Reduced Motion247248### CSS Implementation249250```css251/* Default: full animation */252.element {253 transition: transform 300ms ease-out;254}255256/* Reduced motion: replace with opacity or instant */257@media (prefers-reduced-motion: reduce) {258 .element {259 transition: opacity 200ms ease-out;260 /* Or: transition: none; */261 }262263 /* Kill all animations globally */264 *, *::before, *::after {265 animation-duration: 0.01ms !important;266 animation-iteration-count: 1 !important;267 transition-duration: 0.01ms !important;268 }269}270```271272### Safe vs Harmful Motion273274| Safe | Potentially Harmful | Always Avoid |275|------|-------------------|--------------|276| Opacity fades | Parallax scrolling | Auto-playing video backgrounds |277| Color transitions | Background movement | Infinite looping animations |278| Small scale (<5%) | Zoom animations | Flashing (seizure risk) |279| Very slow movement (500ms+) | Spinning/rotating | Rapid zoom in/out |280| Non-repeating animations | Fast repeated animations | >3 flashes per second |281| | Large moving areas (>1/4 viewport) | Vestibular-triggering patterns |282283### WCAG Compliance284285- **2.3.3**: Motion from interactions can be disabled unless essential286- **2.2.2**: Moving content >5 seconds must be pausable287- **2.3.1**: No content flashes >3 times per second288289## Performance Rules2902911. Animate only `transform` and `opacity` for GPU acceleration2922. Use `will-change` sparingly; remove after animation completes2933. Prefer CSS over JavaScript when animation is predictable2944. Keep per-frame work under ~16ms for 60fps2955. Test on low-powered devices2966. Debounce scroll/resize-triggered animations2977. Use `requestAnimationFrame` for JS animations2988. Virtualize animated lists with 50+ items299300## Pre-Delivery Checklist301302- [ ] `prefers-reduced-motion` respected globally303- [ ] All animations have reduced/no-motion alternative304- [ ] No auto-playing motion over 5 seconds305- [ ] Every interactive element has feedback (hover, active, focus)306- [ ] Disabled states have no animation, reduced opacity307- [ ] Exit animations are shorter than enter (~60-70%)308- [ ] No `linear` easing on UI transitions309- [ ] Only `transform` and `opacity` animated (no width/height/top/left)310- [ ] Stagger delay is 30-50ms per item (not simultaneous)311- [ ] No animation blocks user input312- [ ] Animations are interruptible (user gesture cancels in-progress motion)313- [ ] Motion direction is consistent with navigation model