CSS Animation Best Practices: Timing, Easing, and Accessibility
I just spent a week debugging why my animations felt “off” - they were janky, distracting, and honestly kind of annoying. Turns out I was making every mistake in the book: wrong durations, terrible easing curves, layout-thrashing properties, and zero accessibility support. Let me share what I learned about creating animations that actually enhance user experience.
The Problem: Random Animations Everywhere
I added a bounce effect to my button hover state. It felt playful! Then I animated the width of my sidebar for a smooth transition. Performance tanked. My page load sequence had elements flying in from all directions like a bad PowerPoint presentation. Users with vestibular disorders? I didn’t even think about them.
Here’s the thing: animations need purpose. Every animation should answer “why does this exist?” If the answer is “because it looks cool,” that’s a red flag.
Understanding Animation Categories
Before diving into code, I needed to categorize what animations actually DO:
Animation Purposes├── Entrance│ ├── Page load choreography│ ├── Hero sections│ └── Content reveals├── Micro-interactions│ ├── Button feedback│ ├── Form validation│ └── Toggle switches├── State Transitions│ ├── Show/hide│ ├── Expand/collapse│ └── Loading states├── Navigation│ ├── Page transitions│ ├── Tab switching│ └── Carousel slides└── Delight ├── Empty states ├── Celebrations └── Contextual momentsEach category has different timing needs. A button press needs instant feedback. A modal opening can take a bit longer. Understanding this changed how I approached every animation.
Duration Guidelines: The Goldilocks Zone
I tried 1-second animations. Too slow. Then I tried 50ms animations. Too fast, looked broken. After lots of experimentation, here’s what actually works:
| Duration | Use Case | Why |
|---|---|---|
| 100-150ms | Button press, toggle, instant feedback | Immediate acknowledgment |
| 200-300ms | Hover states, menu open, tooltips | Noticeable but quick |
| 300-500ms | Accordion, modal, drawer | Layout reorganization |
| 500-800ms | Page load entrance, hero reveals | Dramatic moments |
The mistake I kept making? Using the same duration for everything. A 500ms hover feels sluggish. A 100ms modal feels jarring. Context matters.
Easing Curves: Stop Using Bounce
I thought bounce and elastic easing looked fun. They don’t. They feel dated, like 2014 web design. After testing dozens of curves, I found the sweet spot:
:root { /* RECOMMENDED - smooth, professional */ --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
/* AVOID - feel cheap and dated */ /* --bounce: cubic-bezier(0.34, 1.56, 0.64, 1); */ /* --elastic: cubic-bezier(0.68, -0.6, 0.32, 1.6); */}Why ease-out? Objects in the real world decelerate. They don’t bounce or overshoot. Ease-out-quart is my go-to for most interactions - it starts quick (responsive) and ends smooth (polished).
I tried building my own curves manually. Bad idea. Tools like easings.net exist for a reason. Use them.
Performance: 60fps or Bust
My sidebar animation caused the whole page to stutter. DevTools showed me why: I was animating width and left, triggering layout recalculations on every frame.
The rule is simple: only animate transform and opacity. These are GPU-accelerated and don’t trigger layout thrash.
/* WRONG - causes layout thrash */.sidebar-bad { width: 0; transition: width 300ms ease-out;}.sidebar-bad.open { width: 300px; /* Layout recalculation on every frame */}
/* RIGHT - GPU accelerated */.sidebar-good { transform: translateX(-100%); transition: transform 300ms var(--ease-out-quart);}.sidebar-good.open { transform: translateX(0); /* Only compositing, no layout */}At 60fps, each frame has 16ms to complete. Layout properties (width, height, top, left, margin, padding) blow that budget instantly. Transform and opacity? No problem.
I also tried sprinkling will-change: transform everywhere. That caused memory issues. Only use it when you know an animation is coming, not as a blanket optimization.
Accessibility: The Motion Media Query
Here’s what I didn’t know: some users get motion sick from animations. Others have vestibular disorders. macOS has a “Reduce motion” setting. iOS has it too. I was ignoring all of it.
The prefers-reduced-motion media query changes everything:
/* 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; scroll-behavior: auto !important; }}This single media query makes animations instant for users who need it. The 0.01ms duration is intentional - it’s effectively instant but still fires any animation event handlers.
I also learned to design animations so they communicate, not decorate. A loading spinner? Necessary. A bouncing hero title? Probably not. Ask: does this animation help the user understand what’s happening?
A Real-World Example: Button Feedback
Let me show you my before and after for button interactions:
/* BEFORE - slow, bouncy, inaccessible */.button-bad { transition: all 400ms ease-in-out;}.button-bad:hover { transform: scale(1.1);}.button-bad:active { transform: scale(0.9); /* Missing: no reduced motion support */}
/* AFTER - purposeful, smooth, accessible */.button-good { transition: transform 150ms var(--ease-out-quart), background-color 200ms ease-out; will-change: transform; /* Button interactions are common */}.button-good:hover { transform: translateY(-2px); background-color: var(--color-hover);}.button-good:active { transform: translateY(0); /* Press down effect */}.button-good:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px;}
@media (prefers-reduced-motion: reduce) { .button-good { transition: none; }}The difference: 150ms vs 400ms. Subtle translateY vs jarring scale. Focus state for keyboard users. Reduced motion support.
Animation Strategy: Layer by Layer
After all these mistakes, I developed a system. Instead of throwing animations at everything, I plan them:
Animation Planning Checklist├── 1. Hero moment│ └── What's the ONE signature animation?│ (Page load? Key interaction?)├── 2. Feedback layer│ └── Which interactions need acknowledgment?│ (Clicks, hovers, form submissions)├── 3. Transition layer│ └── Which state changes need smoothing?│ (Show/hide, expand/collapse)└── 4. Delight layer └── Where can we surprise and delight? (Empty states, success moments)Most projects only need layers 1-3. Layer 4 (delight) is optional and should be minimal. If I’m adding more than one “signature” animation, I’m probably overdoing it.
Putting It All Together
Here’s a complete example of an entrance animation that follows all these principles:
/* CSS variables for consistent timing */:root { --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); --stagger-delay: 60ms;}
.hero { opacity: 0; transform: translateY(20px); animation: fadeInUp 600ms var(--ease-out-quart) forwards;}
.hero-title { animation-delay: calc(var(--stagger-delay) * 0);}
.hero-subtitle { animation-delay: calc(var(--stagger-delay) * 1);}
.hero-cta { animation-delay: calc(var(--stagger-delay) * 2);}
@keyframes fadeInUp { to { opacity: 1; transform: translateY(0); }}
/* Reduced motion: animations become instant */@media (prefers-reduced-motion: reduce) { .hero { opacity: 1; transform: none; animation: none; }}Notice: only transform and opacity. Staggered timing for visual hierarchy. Ease-out curve. Reduced motion fallback. Duration under 800ms.
Key Takeaways
- Durations: 100-300ms for most things, 500-800ms for entrance only
- Easing: Ease-out-quart/quint/expo are your friends; bounce and elastic are not
- Performance: Only animate transform and opacity for 60fps
- Accessibility: Always include prefers-reduced-motion support
- Purpose: Every animation should enhance understanding, not just decorate
Animations done well feel invisible - users don’t notice them, they just feel the interface is “smooth” or “polished.” Done poorly, they’re distracting at best and accessibility barriers at worst.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments