CSS Animations

Introduction

Transitions are great for simple state changes, but some interactions require multi-step motion: loading indicators, attention pulses, entry effects, and looping decorative feedback. CSS animations provide that control with @keyframes. This chapter explains how to build clear, lightweight animations that improve UX without overwhelming users.

Prerequisites

  • Basic transitions and transforms
  • Familiarity with pseudo-classes and interaction states
  • Basic awareness of motion accessibility concerns

Transition vs Animation

Quick rule:

  • Transition: animate from one state to another when a property changes
  • Animation: run a keyframed timeline that can loop or play independently

Use animations when you need multiple stages or repeated motion.

@keyframes Basics

Define animation steps:

css
@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Code explanation:

  • from is equivalent to 0%
  • to is equivalent to 100%
  • browser interpolates values between these keyframes

Then apply with animation.

Applying an Animation

css
.card {
  animation: fade-in-up 320ms ease-out;
}

Shorthand order typically includes:

  • name
  • duration
  • timing function
  • delay (optional)
  • iteration count (optional)
  • direction/fill/play-state (optional)

Core Animation Properties

Longhand controls:

  • animation-name
  • animation-duration
  • animation-timing-function
  • animation-delay
  • animation-iteration-count
  • animation-direction
  • animation-fill-mode
  • animation-play-state

Equivalent example:

css
.pulse {
  animation-name: pulse;
  animation-duration: 1.2s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

Multi-Step Keyframes

Use percentage keyframes for richer motion:

css
@keyframes pulse {
  0%,
  100% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(1.04);
    opacity: 0.85;
  }
}

This creates a smooth, repeating pulse effect.

Iteration Count

css
.once {
  animation: fade-in-up 300ms ease-out 1;
}
 
.loop {
  animation: pulse 1.2s ease-in-out infinite;
}

Guideline:

  • one-time for entrances/feedback
  • infinite only for meaningful continuous states (loading, recording, attention)

Animation Direction

Useful direction values:

  • normal
  • reverse
  • alternate
  • alternate-reverse

Example:

css
.breathing {
  animation: pulse 1.4s ease-in-out infinite alternate;
}

alternate avoids abrupt jump between loop cycles.

Fill Mode

animation-fill-mode controls style before/after animation.

css
.toast-enter {
  animation: fade-in-up 260ms ease-out forwards;
}

Code explanation:

  • forwards keeps final keyframe styles after animation ends
  • useful for entrance states where end style should persist

Other values include backwards and both.

Delays and Stagger

Small delays can create staggered appearance:

css
.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 60ms; }
.item:nth-child(3) { animation-delay: 120ms; }

Use subtle staggering for lists/cards; avoid long delays that block comprehension.

Play State Control

css
.spinner {
  animation: spin 1s linear infinite;
}
 
.spinner.paused {
  animation-play-state: paused;
}

Useful when pausing UI motion during certain app states.

Common Animation Patterns

1) Loading Spinner

css
@keyframes spin {
  to { transform: rotate(360deg); }
}
 
.spinner {
  width: 20px;
  height: 20px;
  border: 2px solid #cbd5e1;
  border-top-color: #2563eb;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

2) Fade-in Entrance

css
.panel {
  animation: fade-in-up 280ms ease-out;
}

3) Attention Pulse (sparingly)

css
.badge-live {
  animation: pulse 1.4s ease-in-out infinite;
}

Use only for truly important signals.

Animating Multiple Properties

Keep animation focused on cheap, smooth properties where possible:

  • transform
  • opacity

Avoid heavy frequent animation of:

  • layout properties (width, height, top, left)
  • complex filters on large surfaces

This improves perceived smoothness and battery usage.

Tip

Prefer Subtle Motion

Short, meaningful motion improves clarity. Constant or large movement often harms focus.

Accessibility: Reduced Motion

Always respect user motion preferences:

css
@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
    transition: none !important;
  }
}

If full disable is too aggressive for your UI, at least remove non-essential decorative loops.

Common Mistakes

MistakeWhy it hurtsBetter approach
Infinite animation everywhereCognitive overload and distractionReserve loops for meaningful states
Long entrance animationsInterface feels slowKeep most UI motion short (150-400ms)
Animating layout-heavy propertiesJank/performance issuesFavor transform/opacity
No reduced-motion fallbackAccessibility problemAdd prefers-reduced-motion handling
Using motion without UX purposeVisual noiseTie animation to state/feedback meaning

Debugging Animations

When animation is not behaving correctly:

  1. verify animation-name matches keyframes name exactly
  2. check duration is not 0s
  3. inspect whether another rule overrides animation
  4. confirm keyframe property values are animatable
  5. test with DevTools animation inspector/timeline

For state-driven animations, also verify class/state toggles occur as expected.

Mini Exercise

  1. Create a spinner with infinite spin
  2. Create a card entrance animation (fade-in-up)
  3. Add stagger delay to a 4-item list
  4. Add one pulse badge for live status
  5. Implement reduced-motion fallback and verify behavior

FAQ

Should I use transitions or keyframes for hover?

Usually transitions. Keyframes are better for multi-stage or looping motion.

Why does my animation run once and then reset?

Because default fill mode is none. Use animation-fill-mode: forwards if final state should persist.

Can CSS animations run without JavaScript?

Yes. CSS alone can handle many common effects, especially state and loop-based visual feedback.

Is infinite always bad?

No, but it should be intentional. Loading indicators and live-state signals are valid use cases.

What duration feels natural for UI animations?

Often around 150-400ms for entrances and micro-interactions, depending on motion distance and context.