webdev.complete
🎬 Animation & Transitions
🎨CSS Power
Lesson 20 of 117
25 min

Transitions & Keyframes

The cheap-and-fast way to move things.

Static interfaces feel cheap. The difference between a $5 site and a $50,000 product is usually a hundred milliseconds of motion in the right places. CSS gives you two tools for that: transitions (animate between two states) and keyframes (animate through a custom sequence). This lesson teaches both, and also when not to use them.

Transitions: animate between two states

A transition is the easiest kind of animation. You tell CSS "if this property changes, slide between the old and new values." Then you change the value (via :hover, a class swap, whatever) and the browser does the rest.

css
.button {
  background: #4f46e5;
  transform: scale(1);
  transition: transform 200ms ease, background 200ms ease;
}

.button:hover {
  background: #6366f1;
  transform: scale(1.05);
}

The shorthand is transition: <property> <duration> <easing> <delay>. List multiple properties separated by commas. Use allto transition everything (but be careful, this can animate things you didn't want, like text color when you change a theme).

Easing: where motion gets its personality

Easing is the curve that maps elapsed time to animation progress. A linear easing makes things look mechanical. A real-world easing makes them feel physical, like they have weight.

  • ease: starts fast, ends slow. The default. Fine for most.
  • ease-in: starts slow, ends fast. Good for things leaving the screen.
  • ease-out: starts fast, ends slow. Good for things entering the screen.
  • ease-in-out: slow at both ends, fast in the middle. The most "natural" feel.
  • linear: constant speed. Use for things that should feel mechanical (loading bars, progress).

cubic-bezier(): custom easing curves

When the named easings aren't enough, define your own with cubic-bezier(x1, y1, x2, y2). The four numbers are the two control points of a Bezier curve. The Chrome DevTools easing editor is your friend here, but a few classics:

  • cubic-bezier(0.4, 0, 0.2, 1): Material Design's standard curve. Smooth and elegant.
  • cubic-bezier(0.34, 1.56, 0.64, 1): an overshoot, like a spring. Great for popping into existence.
  • cubic-bezier(0.68, -0.55, 0.265, 1.55): a back-and-out (overshoots both ends).

linear() with stops: the new kid

Shipped in 2023: linear() takes a list of stops and creates a piecewise-linear curve. Sounds boring, but it lets you approximate spring animations (which are not Bezier curves) inside pure CSS:

css
.bounce {
  /* A spring-like curve approximated as 8 stops */
  transition: transform 600ms linear(
    0, 0.0001, 0.001, 0.005, 0.02, 0.05, 0.1,
    0.3 30%, 0.6, 0.9, 1.1 60%, 1.0, 0.95, 1.0
  );
}

Tools like linear-easing-generator convert spring physics to linear() output, so you can copy-paste the right curve. No Bezier needed.

Easing function tester
.box { transition: transform 600ms ease-out; }
.box.active { transform: translateX(200px); }

@keyframes: animate through a sequence

When two states aren't enough, define an animation explicitly:

css
@keyframes pulse {
  0%   { transform: scale(1);   opacity: 1;   }
  50%  { transform: scale(1.1); opacity: 0.7; }
  100% { transform: scale(1);   opacity: 1;   }
}

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

The shorthand on the element is animation: <name> <duration> <easing> <delay> <iteration-count> <direction> <fill-mode>. The two you'll mess up most:

  • iteration-count: a number, or infinite.
  • fill-mode: forwards keeps the final state after the animation ends. backwards applies the starting state during any delay. Forgetting forwards is the cause of "why does my element snap back at the end?"

What to animate: transform and opacity, nothing else

Browsers can animate transform and opacity on the GPU, which means smooth 60+ fps even on phones. Almost every other property (width, height, top, left, margin, padding) triggers layout or paint on every frame, which is slow.

The single most common animation bug
Setting transition: left 200ms and animating from left: 0 to left: 200px works but is choppy. Use transition: transform 200ms with translateX(200px) instead. Same visual result, ten times the frame rate.

prefers-reduced-motion: respect the user

Some users get nauseous from motion. Some have vestibular disorders. Their OS lets them say "please cut down on animations," and your CSS can honor that:

css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

That nuclear option disables effectively all motion. A gentler take is to keep the animations but make them shorter and simpler. The principle is: don't move things across the screen, don't spin or wobble, don't parallax. Fades and small color shifts are usually fine.

Performance shortlist

  • Animate transform and opacity. Avoid anything else.
  • Use will-change: transformon elements you're about to animate, but remove it after. Permanent will-change wastes memory.
  • Keep durations under 400ms for most UI transitions. Longer feels sluggish.
  • Use prefers-reduced-motion. Always.

Quiz

Quiz1 / 4

Which two CSS properties can be animated cheaply on the GPU?

Recap

  • Transitions animate between two states triggered by class or pseudo-class changes.
  • @keyframes defines multi-step animations. Apply via animation: on an element.
  • Easing sets the personality. ease-out for things entering, ease-in for leaving, custom cubic-bezier() or linear() when needed.
  • Animate transform and opacity for 60fps. Anything else costs frames.
  • Use animation-fill-mode: forwards to keep the final state.
  • Honor prefers-reduced-motion. Always.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.