Skip to content
C Codeloom
HTML & CSS

CSS Transitions and Keyframe Animations

A practical guide to CSS transitions and @keyframes animations — properties, timing functions, transform-based performance, the animation shorthand, and prefers-reduced-motion.

·7 min read · By Yash Kesharwani
Intermediate 12 min read

What you'll learn

  • How CSS transitions animate property changes
  • transition-property, duration, timing-function, and delay
  • Why transform and opacity are the fast properties
  • Writing @keyframes for multi-step animations
  • The animation shorthand and the properties it bundles
  • Respecting prefers-reduced-motion

Prerequisites

  • Comfort with CSS selectors and the box model
  • Helpful: CSS Grid for the layout in examples

A static page does its job. A page that moves — buttons that respond to hover, modals that ease open, lists that animate as they reorder — feels alive. CSS gives you two tools to make things move: transitions for going between states, and @keyframes animations for choreography. Both are easier than they look and both can ruin performance if used carelessly.

This post covers the working knowledge you need to ship motion without ruining frame rates.

Transitions: animating between states

A transition tells the browser: “When this property changes, do not snap to the new value — interpolate over time.”

.button {
  background-color: #2563eb;
  transition: background-color 200ms ease;
}

.button:hover {
  background-color: #1d4ed8;
}

Without the transition, the hover color change is instant. With it, the browser smoothly animates the background color over 200 milliseconds. Nothing else changes — you write the start state and the end state, and CSS interpolates.

You can transition almost any animatable property:

.card {
  transition: transform 250ms ease, box-shadow 250ms ease;
}
.card:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 20px rgb(0 0 0 / 0.1);
}

The four transition properties

The shorthand expands to four longhand properties:

  • transition-property — which properties to animate. all works but is wasteful. List the specific ones.
  • transition-duration — how long the change takes. 200ms to 300ms covers most UI feedback.
  • transition-timing-function — the curve. ease, ease-in, ease-out, ease-in-out, linear, or cubic-bezier(…).
  • transition-delay — wait before starting. Useful for staggered effects.
.menu-item {
  transition-property: transform, opacity;
  transition-duration: 200ms;
  transition-timing-function: ease-out;
  transition-delay: 0ms;
}

The shorthand puts duration first and delay second when both are present:

.menu-item {
  transition: transform 200ms ease-out 0ms,
              opacity   200ms ease-out 0ms;
}

Timing functions you will actually use

  • ease — the default. Slight ease in, slight ease out. Fine for most things.
  • ease-out — slow at the end. Great for things appearing.
  • ease-in — slow at the start. Great for things leaving.
  • linear — constant speed. Use for loading spinners and progress bars, not for UI.
  • cubic-bezier(0.2, 0.8, 0.2, 1) — a punchier curve many design systems use for “spring-like” motion.

A rule of thumb: when in doubt, use ease-out at 200ms. It feels snappy and responsive without being abrupt.

The performance rule: transform and opacity

Browsers paint pages in stages — layout (computing positions), paint (filling pixels), composite (combining layers). Most CSS properties retrigger one or more of these stages every frame, which is expensive.

Two properties skip layout and paint entirely: transform and opacity. The browser handles them on the GPU on a separate composite layer, hitting a smooth 60fps on almost any device.

/* Bad: animates layout — triggers reflow every frame */
.card {
  transition: top 300ms ease;
}
.card:hover { top: -4px; }

/* Good: animates transform — composited */
.card {
  transition: transform 300ms ease;
}
.card:hover { transform: translateY(-4px); }

Same visual result, vastly different cost. The rule:

  • For position changes, animate transform: translate(...) instead of top/left/margin.
  • For size changes, animate transform: scale(...) instead of width/height.
  • For show/hide, animate opacity (and visibility with a delay, if needed).

Try it. Pick a hover effect on your site that animates width, height, top, or margin. Rewrite it with transform. Open devtools’ Rendering panel and watch the paint flashes disappear.

Keyframes: choreographed animations

Transitions move between two states. Keyframe animations can move through many states and play on their own — without needing a trigger like :hover.

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

.notification {
  animation: pulse 1500ms ease-in-out infinite;
}

You define a set of percentage stops; the browser interpolates between them. 0% and 100% can be replaced with from and to.

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

.toast {
  animation: fade-in 250ms ease-out;
}

The animation shorthand

The animation property bundles eight longhand values. You rarely set them all:

.thing {
  animation: name duration timing-function delay
             iteration-count direction fill-mode play-state;
}

The ones you reach for in practice:

  • animation-name — the keyframes block to use.
  • animation-duration — how long one iteration lasts.
  • animation-timing-function — same curves as transitions.
  • animation-delay — wait before starting.
  • animation-iteration-count — a number, or infinite.
  • animation-fill-modeforwards keeps the final state after the animation ends, which is almost always what you want.
.toast {
  animation: fade-in 250ms ease-out forwards;
}

Without forwards, the toast snaps back to opacity: 0 after the animation finishes. With it, the toast stays visible.

Staggering with delays

Multiple elements with incremental delays create a satisfying staggered effect:

.item {
  opacity: 0;
  animation: fade-in 300ms ease-out forwards;
}

.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 80ms; }
.item:nth-child(3) { animation-delay: 160ms; }
.item:nth-child(4) { animation-delay: 240ms; }

For long lists, generate the delays with a CSS custom property and calc():

.item {
  animation: fade-in 300ms ease-out forwards;
  animation-delay: calc(var(--i) * 60ms);
}
<li class="item" style="--i: 0">One</li>
<li class="item" style="--i: 1">Two</li>
<li class="item" style="--i: 2">Three</li>

prefers-reduced-motion

Some users get motion sickness, vertigo, or distraction from screen animations. Operating systems expose a “reduce motion” preference, and CSS lets you read it with a media query.

.card {
  transition: transform 250ms ease;
}
.card:hover {
  transform: translateY(-4px);
}

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

A common compromise is to keep crossfades (which do not move) but disable translations and scales. Test with your OS setting toggled on — Mac, Windows, iOS, and Android all have the toggle.

/* Disable all animations and transitions by default for users who prefer it */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

This nuclear option ships in many starter templates and is a reasonable default.

Try it. Turn on “Reduce motion” in your OS settings. Visit a site that uses lots of animation. Notice how much of the motion was decoration rather than communication.

Choosing transition or animation

Use a transition when motion is triggered by a state change — hover, focus, a class being added or removed.

Use @keyframes when:

  • The motion needs to play on its own (page load, attention pulse, loading spinner).
  • The motion has more than two stages.
  • You want fine control over the curve at each step.

You can mix both freely on the same element.

Recap

  • Transitions interpolate between states. Set transition-property, duration, timing-function.
  • Prefer animating transform and opacity — they are the cheap properties.
  • @keyframes plus the animation shorthand drive multi-step or self-playing motion.
  • animation-fill-mode: forwards keeps the final state.
  • Always respect prefers-reduced-motion.

Next steps

Questions or feedback? Email codeloomdevv@gmail.com.