Skip to content

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 moments

Each 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:

DurationUse CaseWhy
100-150msButton press, toggle, instant feedbackImmediate acknowledgment
200-300msHover states, menu open, tooltipsNoticeable but quick
300-500msAccordion, modal, drawerLayout reorganization
500-800msPage load entrance, hero revealsDramatic 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:

easing-curves.css
: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.

performance-demo.css
/* 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:

accessibility.css
/* 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:

button-animation.css
/* 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:

entrance-animation.css
/* 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

  1. Durations: 100-300ms for most things, 500-800ms for entrance only
  2. Easing: Ease-out-quart/quint/expo are your friends; bounce and elastic are not
  3. Performance: Only animate transform and opacity for 60fps
  4. Accessibility: Always include prefers-reduced-motion support
  5. 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