HTML & CSS: The Document ModelVisual DesignLesson 15 of 18

Transitions and Animations

Motion communicates state changes and guides attention. But poor animation degrades experience: sluggish interactions feel unresponsive, excessive motion causes fatigue. This lesson covers when to use animation, how to make it feel natural, and performance constraints that keep interfaces fast.

When Not to Animate

Before learning how to animate, understand when animation is wrong.

The frequency principle: elements used 100+ times per day should not animate. Every button click, every menu toggle, every tab switch compounds. A 200ms animation feels fine once. After 100 interactions, those 20 seconds accumulate into noticeable delay. High-frequency actions must be instant.

Examples of high-frequency interactions:

  • Clicking buttons in a form
  • Switching tabs
  • Opening/closing dropdowns
  • Selecting items in a list
  • Keyboard navigation

These should have no delay. State changes should be immediate.

Use animation for:

  • Page transitions (low frequency)
  • Modal dialogs (infrequent, high attention cost)
  • Success/error feedback (infrequent, needs attention)
  • First-time experiences (happens once)
  • Decorative elements that don't block interaction

Transitions

Transitions animate property changes over time:

.button {
  background: blue;
  transition: background 0.2s;
}

.button:hover {
  background: darkblue;
}

When background changes, it animates over 0.2 seconds instead of switching instantly.

The transition Shorthand

The transition property combines four sub-properties:

.element {
  transition: property duration timing-function delay;
}

Examples:

/* Single property */
.button {
  transition: background 0.2s ease-out;
}

/* Multiple properties */
.button {
  transition:
    background 0.2s ease-out,
    transform 0.2s ease-out;
}

/* All properties (avoid this) */
.button {
  transition: all 0.2s;
}

Transition Properties

You can transition most CSS properties, but not all transitions perform well:

Fast (GPU-accelerated):

  • transform (translate, rotate, scale)
  • opacity

Slow (causes repaints or reflows):

  • width, height
  • top, left, margin, padding
  • background (acceptable for small elements)
  • color (acceptable for text)

Use transform instead of position properties:

/* Bad - animates layout */
.panel {
  left: -300px;
  transition: left 0.3s;
}
.panel.open {
  left: 0;
}

/* Good - animates transform */
.panel {
  transform: translateX(-100%);
  transition: transform 0.3s;
}
.panel.open {
  transform: translateX(0);
}

Both achieve the same visual result, but transform runs on the GPU and doesn't trigger layout recalculation.

Timing Functions

Timing functions (easing) control how property values change over time. The wrong easing makes animation feel mechanical or sluggish.

The Easing Blueprint

Use these easing functions for different interaction types:

ease-out: Elements entering the viewport or appearing

.modal {
  opacity: 0;
  transform: scale(0.9);
  transition:
    opacity 0.25s ease-out,
    transform 0.25s ease-out;
}

.modal.open {
  opacity: 1;
  transform: scale(1);
}

Starts fast, decelerates at the end. This feels natural for elements coming to rest.

ease-in-out: Elements moving from one position to another

.slider-item {
  transition: transform 0.3s ease-in-out;
}

Accelerates at the start, decelerates at the end. Use for movement where the element stays visible throughout.

ease: Hover and interaction feedback

.button {
  transition: background 0.15s ease;
}

The default easing. Starts fast, slight deceleration. Good for subtle interactive feedback.

Custom Timing with cubic-bezier()

Create custom easing curves with cubic-bezier():

.element {
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

The four numbers define two control points on a cubic curve. Most designers use tools to generate these values rather than writing them manually. Stick to standard easing keywords unless you need precise custom motion.

Steps

The steps() function creates frame-by-frame animation:

.sprite {
  background: url('sprite-sheet.png');
  animation: sprite 1s steps(10) infinite;
}

@keyframes sprite {
  to {
    background-position: -1000px 0;
  }
}

This is useful for sprite sheet animations where you want to jump between discrete frames rather than smoothly interpolate.

Duration

Duration determines how long an animation takes. Too fast is jarring, too slow is tedious.

Duration guidelines:

  • Micro interactions (100–150ms): Hover states, focus rings, checkbox toggles
  • Standard transitions (150–250ms): Buttons, dropdowns, tab switches
  • Modals and overlays (200–300ms): Dialogs, sheets, popovers
  • Page transitions (300–400ms): Route changes, full-screen transitions
/* Hover feedback - fast */
.button:hover {
  background: darkblue;
  transition: background 0.15s;
}

/* Dropdown menu - standard */
.menu {
  transition: opacity 0.2s;
}

/* Modal dialog - deliberate */
.modal {
  transition: transform 0.25s;
}

Longer animations work for infrequent, high-impact moments. Shorter animations work for frequent interactions. Remember the frequency principle: 100ms × 100 clicks = 10 seconds of accumulated delay.

Button Press Feel

Make buttons feel responsive with scale on press:

.button {
  transition: transform 0.1s ease;
}

.button:active {
  transform: scale(0.97);
}

The slight squish on press provides tactile feedback. This should be fast (100ms) and subtle (0.95–0.98 scale). Don't animate :active state itself—apply the transform instantly, animate the transition back to normal.

Keyframe Animations

Keyframe animations define multi-step sequences:

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.element {
  animation: fadeInUp 0.3s ease-out;
}

Keyframes create named animations that you apply with the animation property. You can define intermediate steps:

@keyframes pulse {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.05);
  }
  100% {
    transform: scale(1);
  }
}

.notification {
  animation: pulse 0.6s ease-in-out;
}

Animation Properties

The animation property has multiple sub-properties:

.element {
  animation-name: fadeIn;
  animation-duration: 0.3s;
  animation-timing-function: ease-out;
  animation-delay: 0.1s;
  animation-iteration-count: 1;
  animation-direction: normal;
  animation-fill-mode: forwards;
}

Shorthand:

.element {
  animation: fadeIn 0.3s ease-out 0.1s 1 normal forwards;
}

Common properties:

  • animation-iteration-count: infinite - Loop forever
  • animation-fill-mode: forwards - Keep final state after animation ends
  • animation-fill-mode: backwards - Apply first keyframe before animation starts
  • animation-direction: alternate - Reverse direction each iteration

Pairing Modals and Overlays

When animating modals, animate the overlay and modal with the same duration and easing:

.overlay {
  transition: opacity 0.25s ease-out;
}

.modal {
  transition:
    opacity 0.25s ease-out,
    transform 0.25s ease-out;
}

Mismatched timing looks broken. If the overlay fades in 200ms but the modal takes 300ms, the experience feels uncoordinated. Paired elements should move together.

Performance: Transform and Opacity Only

For smooth 60fps animation, only animate transform and opacity:

/* Good - GPU accelerated */
.element {
  transition:
    transform 0.3s,
    opacity 0.3s;
}

/* Bad - causes layout recalculation */
.element {
  transition:
    width 0.3s,
    height 0.3s,
    top 0.3s;
}

Animating width, height, or position properties forces the browser to recalculate layout for every frame. This causes jank on slower devices. Use transform: scale() instead of animating dimensions, transform: translate() instead of animating position.

The will-change Property

will-change hints to the browser that a property will animate:

.modal {
  will-change: transform, opacity;
}

This tells the browser to optimize for animating those properties. But don't overuse it. will-change consumes memory. Apply it only to elements that will actually animate soon, and remove it when animation is done:

.modal {
  /* Don't add will-change here */
}

.modal.opening {
  will-change: transform, opacity;
}

.modal.open {
  will-change: auto; /* Remove optimization */
}

For most cases, you don't need will-change. The browser optimizes transform and opacity automatically.

Reduced Motion

Some users experience nausea or disorientation from animation. Respect their preferences:

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

This disables all animations for users who've enabled reduced motion in their OS settings. You can be more selective:

.modal {
  transition: transform 0.25s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .modal {
    transition: none;
  }
}

Every animation you create must respect prefers-reduced-motion. This is an accessibility requirement, not optional. Users with vestibular disorders, ADHD, or motion sensitivity need this.

Scroll-Driven Animations

CSS can drive animations based on scroll position:

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

.reveal {
  animation: fadeIn linear;
  animation-timeline: view();
}

animation-timeline: view() makes the animation progress as the element scrolls into view. This replaces JavaScript scroll listeners for common reveal effects.

Control the scroll range:

.reveal {
  animation: fadeIn linear;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

The animation runs as the element enters the viewport (entry 0% to entry 100%). You can specify ranges like cover 0% cover 50% (first half of element covering viewport) or exit 0% exit 100% (element leaving viewport).

Check Your Understanding

Which timing function should you use for elements entering the viewport?

Not quite. The correct answer is highlighted.

Why should you avoid animating width and height?

Not quite. The correct answer is highlighted.

According to the frequency principle, when should you NOT use animation?

Not quite. The correct answer is highlighted.
For smooth 60fps animation, only animate and opacity.
Not quite.Expected: transform

Practice

Summary

  • Frequency principle: Don't animate elements used 100+ times per day—accumulated delay degrades experience
  • Use animation for: Page transitions, modals, feedback, first-time experiences (low-frequency moments)
  • Transition shorthand: transition: property duration timing-function delay
  • Never transition all: Always specify exact properties to avoid performance issues
  • Performance: Only animate transform and opacity for GPU acceleration and smooth 60fps
  • Easing blueprint: Use ease-out for enter/exit, ease-in-out for movement, ease for hover
  • Never use ease-in: Starts slow, makes interactions feel sluggish and unresponsive
  • Duration guidelines: Micro 100–150ms, standard 150–250ms, modals 200–300ms, pages 300–400ms
  • Button press feel: Use transform: scale(0.97) on :active for tactile feedback
  • Paired elements: Animate modal and overlay with matching duration and easing
  • Keyframe animations: Use @keyframes for multi-step sequences with animation property
  • will-change: Only use when needed, remove after animation completes
  • Reduced motion: Every animation must respect prefers-reduced-motion for accessibility
  • Scroll animations: Use animation-timeline: view() for scroll-driven effects