TH The Sass Way
CSS Techniques

CSS Animations Tutorial: Keyframes, Transitions, and Timing

CSS Animations Tutorial: Keyframes, Transitions, and Timing
tldrCSS animations come in two forms: transitions and keyframe animations. A transition animates a property from one value to another when a state changes, like a hover or class toggle. A keyframe animation runs a named @keyframes sequence with multiple steps that can loop, reverse, and play on its own. Use transitions for simple state changes and keyframes for multi-step or self-starting motion.

CSS animations in one minute

CSS gives you two built-in ways to move things: transitions and keyframe animations. A transition animates a property from its current value to a new one when something changes — a hover, a class toggle, a focus. A keyframe animation runs a named sequence you define ahead of time, with as many steps as you want, and it can loop, reverse, and run on its own without any user interaction.

Here is the whole idea in two snippets. First, a transition:

.button {
  background: #2563eb;
  transition: background 200ms ease;
}
.button:hover {
  background: #1e40af;
}

And the same kind of motion as a keyframe animation:

@keyframes pulse {
  from { transform: scale(1); }
  to   { transform: scale(1.05); }
}
.button {
  animation: pulse 600ms ease-in-out infinite alternate;
}

That's the fork in the road. The rest of this tutorial explains when to reach for each, every property that controls them, the timing functions that make motion feel right, and how to keep animations smooth and accessible.

Transitions: animate a change of state

A transition needs two things: a starting value already on the element, and a new value applied by some state change. If both exist, the browser animates between them instead of snapping.

The shorthand takes up to four parts, in this order:

transition: <property> <duration> <timing-function> <delay>;
.card {
  transform: translateY(0);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
  transition: transform 250ms ease-out, box-shadow 250ms ease-out;
}
.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25);
}

Note that you can list several transitions separated by commas, each with its own duration and easing. That is almost always better than transition: all, which forces the browser to watch every animatable property and can accidentally animate things you didn't mean to.

The four transition properties

Property What it controls Example
transition-property Which properties animate transform, opacity
transition-duration How long it takes 200ms, 0.3s
transition-timing-function The acceleration curve ease, cubic-bezier(...)
transition-delay Wait before starting 100ms

Durations accept seconds (0.3s) or milliseconds (300ms); both are valid, and milliseconds tend to read more clearly for short UI motion. Anything under roughly 100ms feels instant, and most interface transitions land well between 150ms and 400ms. Longer than about 500ms starts to feel sluggish for a hover.

What you can and can't transition

Not every property animates. A value has to have a defined "in-between" for the browser to interpolate it. Numbers, lengths, colors, and transforms interpolate cleanly. Discrete values like display historically do not — which is why the classic trick is to animate opacity and visibility instead of display: none. Modern browsers now support transitioning to display: none using transition-behavior: allow-discrete, but check support before relying on it.

Keyframe animations: define a full sequence

When you need more than a single A-to-B change — a loop, multiple stops, motion that starts on its own — use @keyframes. You define the sequence once and attach it with the animation property.

@keyframes slide-in {
  0% {
    opacity: 0;
    transform: translateX(-20px);
  }
  100% {
    opacity: 1;
    transform: translateX(0);
  }
}
.panel {
  animation: slide-in 400ms ease-out both;
}

Keyframes use percentages from 0% to 100% (or the keywords from and to, which mean 0% and 100%). You can add as many stops as you like, and several selectors can share one rule:

@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-16px); }
}

The animation sub-properties

The animation shorthand bundles eight longhand properties. You rarely need all of them, but knowing each one is what separates copy-pasting from actually controlling motion.

Property Purpose Common values
animation-name The @keyframes to run your rule name
animation-duration Length of one cycle 600ms, 2s
animation-timing-function Easing curve ease-in-out, linear
animation-delay Wait before first run 200ms
animation-iteration-count How many times 1, 3, infinite
animation-direction Play order normal, alternate, reverse
animation-fill-mode State before/after running none, forwards, both
animation-play-state Running or paused running, paused

A full shorthand reads left to right — the first time value is duration, the second is delay:

.spinner {
  animation: spin 1s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

Fill mode is the one people miss

By default, an element snaps back to its unanimated styles the instant the animation ends. animation-fill-mode fixes that:

If an element flickers back to its original position when the animation stops, you almost certainly want forwards or both.

Timing functions: why motion feels right or wrong

The timing function is the difference between motion that feels natural and motion that feels robotic. It maps elapsed time to progress, controlling acceleration.

Keyword Feel Good for
linear Constant speed Spinners, progress bars
ease Slow-fast-slow (default) General UI
ease-in Starts slow Elements leaving
ease-out Ends slow Elements entering
ease-in-out Slow at both ends Loops, emphasis

For anything else, write your own curve with cubic-bezier(x1, y1, x2, y2). The four numbers are the control points; the X values must stay between 0 and 1, while Y can overshoot to create a springy, bouncing feel:

.modal {
  transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

There is also steps(n), which jumps through the animation in discrete frames instead of interpolating smoothly — perfect for sprite-sheet animation or a typewriter effect:

@keyframes typing {
  from { width: 0; }
  to   { width: 20ch; }
}
.caption {
  width: 20ch;
  overflow: hidden;
  white-space: nowrap;
  animation: typing 2s steps(20) forwards;
}

Transition vs animation: which should you use?

They overlap, but the choice is usually clear once you name what you need.

Question Transition Keyframe animation
Triggered by a state change (hover, focus, class)? Yes Optional
Needs to run on its own, no interaction? No Yes
More than a start and end point? No Yes
Needs to loop or reverse automatically? No Yes
Simplest possible code? Yes

Rule of thumb: if you're animating the response to something the user did and it's a single A-to-B change, use a transition. If the motion is a multi-step sequence, loops, or plays automatically, use a keyframe animation.

Performance: animate transform and opacity

The single most important performance rule in CSS animation is this: animate transform and opacity, and avoid animating layout properties.

When you animate width, height, top, left, margin, or padding, the browser has to recalculate the position and size of elements — a step called layout (or reflow) — on every frame. That is expensive and causes jank. transform and opacity, by contrast, can be handled by the compositor, often on the GPU, without touching layout at all.

So instead of animating position with left, animate it with transform: translateX(). Instead of animating size with width, use transform: scale().

/* Janky: triggers layout every frame */
.box { transition: left 300ms ease; }

/* Smooth: composited, no layout */
.box { transition: transform 300ms ease; }

The will-change property can hint to the browser to prepare an element for animation, but use it sparingly — applying it to too many elements wastes memory and can hurt performance:

.menu {
  will-change: transform;
}

Add it only to elements you're about to animate, and consider removing it after. Because animations often lean on custom properties and gradients for polish, it's worth pairing this with a solid grasp of CSS custom properties and CSS gradients so your motion and your visuals stay in sync.

Accessibility: respect reduced motion

Animation can cause real harm. For people with vestibular disorders, large or spinning motion can trigger dizziness and nausea. Operating systems expose a "reduce motion" setting, and CSS lets you honor it with a media query:

@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;
  }
}

That blanket reset is a common starting point, but the better practice is to design motion that degrades gracefully — keep essential feedback like a color change while removing large movement:

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

The goal isn't to strip all feedback — it's to remove the motion that can make someone physically unwell. This kind of viewport- and preference-aware thinking is core to responsive web design, where the "right" experience depends on the user and their device, not a single fixed design.

A complete, copy-pasteable example

Here is a card that fades and slides in on load, lifts on hover, and respects reduced-motion preferences — every technique from this guide in one place.

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

.feature-card {
  animation: fade-up 500ms ease-out both;
  transition: transform 200ms ease, box-shadow 200ms ease;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
}

.feature-card:hover {
  transform: translateY(-6px);
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.2);
}

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

Common mistakes to avoid

Get comfortable with transitions first — they cover the majority of real-world interface motion — then layer in keyframe animations when you need loops, sequences, or self-starting effects. Keep every animation on transform and opacity, honor prefers-reduced-motion, and your interfaces will feel fast and polished without a single line of JavaScript.

FAQ

What is the difference between transition and animation in CSS?

A transition animates a single change between two states and needs a trigger, such as a hover or a toggled class. A keyframe animation defines a full sequence with @keyframes, can have many steps, loop, reverse, and run automatically without any interaction. Use transitions for simple state changes; use animations for multi-step or self-starting motion.

How do CSS keyframes work?

You define a sequence with @keyframes, using percentages from 0% to 100% (or the from and to keywords) to set styles at each stop. You then attach it to an element with the animation property, specifying the name, duration, timing function, and how many times it runs. The browser interpolates smoothly between the keyframe stops you defined.

Which CSS properties are best to animate for performance?

Animate transform and opacity. Both can be handled by the compositor, often on the GPU, without recalculating page layout, so they stay smooth at 60fps. Avoid animating width, height, top, left, margin, or padding, since those trigger layout recalculation on every frame and cause visible jank. Replace position changes with translate() and size changes with scale().

How do I make a CSS animation loop forever?

Set animation-iteration-count to infinite, either as a longhand property or inside the animation shorthand. For example, animation: spin 1s linear infinite runs the spin keyframes endlessly. Add the alternate direction value to make it play forward then backward on each cycle, which is useful for smooth pulsing or breathing effects.

How do I respect users who prefer reduced motion?

Use the prefers-reduced-motion media query. Wrap a rule in @media (prefers-reduced-motion: reduce) and either disable transitions and animations or drastically shorten their duration. This honors the operating-system setting used by people with vestibular disorders, for whom large or spinning motion can trigger dizziness and nausea. Keep essential feedback like color changes while removing large movement.

What does animation-fill-mode do?

It controls an element's styles before and after the animation runs. By default an element snaps back to its unanimated state when the animation ends. Setting fill-mode to forwards keeps the final keyframe's styles, backwards applies the first keyframe during any delay, and both does both. Use forwards or both when an element should stay in its final animated position.