TH The Sass Way
CSS Techniques

CSS Animations Tutorial: Keyframes, Transitions, and Timing

CSS Animations Tutorial: Keyframes, Transitions, and Timing
tldrA CSS keyframe animation combines an @keyframes rule with an animation declaration. Keyframes define property values at percentages from 0% to 100%; animation sets the name, duration, easing, delay, iterations, direction, fill mode, and play state. Use transitions for interpolation after a state change. Use keyframes for multiple stops, repetition, reversal, or sequences that start without a new state change.

CSS keyframe animations in one minute

A CSS keyframe animation has two parts: an @keyframes rule that defines property values at points in a sequence, and an animation declaration that assigns the sequence to an element. Use a transition when a property should interpolate after a state change such as :hover. Use keyframes for multiple stops, repetition, reversal or motion that starts without a new state change.

Here is the smallest useful keyframe example:

@keyframes pulse {
  from { transform: scale(1); }
  to   { transform: scale(1.05); }
}

.button {
  animation: pulse 600ms ease-in-out infinite alternate;
}

pulse names the sequence. 600ms is one iteration, ease-in-out controls progress within it, infinite repeats it, and alternate reverses every other iteration. The browser interpolates between scale(1) and scale(1.05).

When should you use a transition instead?

A transition responds to a change between an element's old and new computed values. It needs a starting value, a changed value and a property that can transition.

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

.button:hover,
.button:focus-visible {
  background: #1e40af;
}

The shorthand order is:

transition: <property> <duration> <timing-function> <delay>;

Several transitions can be comma-separated:

.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);
}

Name the properties rather than using transition: all. all is valid, but it can animate a later property change that was never meant to move.

Transition property Purpose Example
transition-property Properties to watch transform, opacity
transition-duration Time from start to end 200ms, 0.3s
transition-timing-function Progress curve ease, cubic-bezier(...)
transition-delay Wait before starting 100ms

Seconds and milliseconds are equivalent units: 0.3s equals 300ms. As an editorial starting point, motion under roughly 100ms reads as nearly immediate, many interface transitions fit between 150ms and 400ms, and a 500ms hover can feel slow. Those are design starting points, not platform rules; test the actual interaction.

How does the @keyframes rule work?

The CSS Animations Level 1 specification defines keyframe selectors as percentages from 0% through 100%, with from and to as aliases for the endpoints. A multi-stop sequence looks like this:

@keyframes slide-in {
  0% {
    opacity: 0;
    transform: translateX(-20px);
  }

  70% {
    transform: translateX(4px);
  }

  100% {
    opacity: 1;
    transform: translateX(0);
  }
}

.panel {
  animation: slide-in 400ms ease-out both;
}

Multiple selectors can share declarations:

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

If a property is missing from the 0% or 100% keyframe, the browser uses its underlying computed value for that endpoint when it can animate the property. That can be convenient, but explicit endpoints are easier to review when another rule later changes the base style.

Keyframe declarations do not participate in the normal cascade in the same way as ordinary style rules. Do not use !important inside @keyframes; the animation specification says those declarations are ignored.

What do the animation properties control?

The familiar Level 1 shorthand represents eight longhands:

Property Purpose Example values
animation-name Selects the @keyframes rule slide-in
animation-duration Length of one cycle 600ms, 2s
animation-timing-function Progress within a segment ease-out, linear
animation-delay Offset before the active interval 200ms, -200ms
animation-iteration-count Number of cycles 1, 3, infinite
animation-direction Playback direction normal, reverse, alternate
animation-fill-mode Effect before or after playback none, forwards, both
animation-play-state Runs or pauses the animation running, paused

The shorthand grammar is flexible, but time values have a fixed interpretation: the first time is duration and the second is delay.

.notice {
  animation: slide-in 400ms ease-out 150ms 1 normal both;
}

A duration cannot be negative. Its initial value is 0s, which is a common reason correctly named keyframes appear to do nothing. A delay may be negative. animation-delay: -200ms starts immediately as if the animation had already run for 200 milliseconds; it does not wait a negative amount of wall-clock time.

Why does an element snap back after the animation?

animation-fill-mode controls when the animation effect applies outside its active interval:

This explains the snap, but forwards should not become a substitute for durable component state. If the final appearance represents a real open, closed, saved or selected state, set that state in the component's ordinary CSS and use animation only for the route between states.

Direction also changes which endpoint counts as the beginning or end. With reverse, the to keyframe supplies the starting effect. With alternate, odd and even iterations run in opposite directions.

How do timing functions work between keyframes?

An easing function maps elapsed time to animation progress. These keywords cover most interface motion:

Value Behavior Common use
linear Constant progress rate Indeterminate spinners
ease Default cubic curve General transitions
ease-in Slow start An element leaving
ease-out Slow finish An element entering
ease-in-out Slow start and finish Reversing motion

Custom curves use cubic-bezier(x1, y1, x2, y2). The CSS Easing Functions Level 2 specification requires both X coordinates to remain within 0 and 1; the Y coordinates may fall outside that range.

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

steps() produces discrete jumps rather than continuous interpolation. A 20-character reveal can use 20 steps:

@keyframes typing {
  from { width: 0; }
  to   { width: 20ch; }
}

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

An animation-timing-function inside a keyframe controls the segment that starts at that keyframe and runs to the next one. This permits different easing on approach and departure without splitting the animation into separate names.

Can CSS transition display: none now?

display is discrete; there are no intermediate display values. CSS Transitions Level 2 introduces transition-behavior: allow-discrete, and @starting-style supplies a before-change style when an element was not previously rendered.

.popover {
  display: block;
  opacity: 1;
  transition:
    opacity 200ms ease,
    display 200ms allow-discrete;
}

.popover.is-closed {
  display: none;
  opacity: 0;
}

@starting-style {
  .popover {
    opacity: 0;
  }
}

This is progressive CSS, not permission to assume every target browser implements the same draft feature set. Check support for the browsers in the project, and retain a usable fallback. opacity alone does not remove an element from layout or interaction, so an older fallback may also need visibility, hidden or script-managed state according to the component.

Which properties should you animate for performance?

Start with transform and opacity, then measure. Google's high-performance animation guide recommends checking rendering-pipeline cost before animating other properties because layout- or paint-triggering changes are harder to keep smooth.

Do not turn that into a false guarantee. Whether an effect is composited depends on the browser, element and surrounding styles. Use browser developer tools to inspect frames, layout, paint and layer behavior on representative devices.

Prefer this:

.box {
  transform: translateX(0);
  transition: transform 300ms ease;
}

.box.is-open {
  transform: translateX(240px);
}

over animating left when the intended result is purely visual movement. Width, height, margin and positional properties can affect layout. Sometimes layout really must change; test it rather than disguising a semantic size change with a transform.

will-change is a hint, not a required animation ingredient:

.menu {
  will-change: transform;
}

The W3C will-change specification explicitly warns against applying it across too many properties or elements because the optimizations consume resources. Add it only when measurement shows a need, apply it shortly before the change when practical, and remove it after the element stops changing.

Our guides to CSS custom properties and CSS gradients cover two common inputs to animation systems. Keep duration and distance tokens named, but do not assume every custom property will interpolate; registration and the property's value type matter.

How should reduced motion be implemented?

The prefers-reduced-motion media feature has two values: no-preference and reduce. Media Queries Level 5 defines reduce as a request to remove or replace non-essential motion that can cause discomfort or distraction.

The safest default is no transform animation, then opt into it only when no reduced-motion preference is expressed:

.feature-card {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: no-preference) {
  .feature-card {
    animation: fade-up 500ms ease-out both;
    transition: transform 200ms ease, box-shadow 200ms ease;
  }

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

Do not treat the popular global 0.01ms !important reset as a complete accessibility solution. It can preserve a rapid flash of the same movement, override component behavior and interfere with code that listens for animation completion. Remove or replace non-essential motion deliberately, while keeping essential state feedback available without movement.

WCAG 2.2 adds separate obligations that a media query does not erase. Moving, blinking or scrolling information that starts automatically, lasts more than five seconds and appears alongside other content needs a pause, stop or hide mechanism unless essential. Content must also stay within the three-flashes-or-below-threshold criterion. These are design and testing requirements, not optional polish.

A complete accessible card example

This preserves the original fade-and-lift example while making the static state the baseline:

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

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.feature-card {
  opacity: 1;
  transform: none;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
}

@media (prefers-reduced-motion: no-preference) {
  .feature-card {
    animation: fade-up 500ms ease-out both;
    transition: transform 200ms ease, box-shadow 200ms ease;
  }

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

Keyboard focus receives the same non-essential enhancement as hover. The card remains visible and understandable when animation does not run.

Why is a CSS animation not working?

Check these in order:

  1. Confirm animation-name exactly matches an @keyframes name.
  2. Inspect computed animation-duration; the initial value is 0s.
  3. Confirm the selector actually matches and is not inside an inactive media query.
  4. Check whether the animated property can interpolate between the supplied values.
  5. Add explicit 0% and 100% values to expose an unexpected underlying style.
  6. Check whether display: none, DOM removal or a class change cancels the effect.
  7. Inspect animation-play-state, iteration count, delay and reduced-motion rules.
  8. Use the browser's animation and performance panels instead of judging only by eye.

For transitions, confirm that the base style existed before the new style was applied. If both values arrive in the same style update, there may be no prior change for the browser to transition from.

The durable rule is simple: transitions connect states; keyframes define sequences. Specify the endpoints, parse the shorthand deliberately, measure rendering cost, and design a complete experience for people who request less motion.

An independent publication. Not affiliated with any prior owner of this domain.

FAQ

How do I write a CSS keyframe animation?

Define a named sequence with @keyframes, using from and to or percentages between 0% and 100%. Apply it with animation, for example animation: slide-in 400ms ease-out both. The first time in the shorthand is duration; a second time is delay. Explicit endpoints make the sequence easier to debug.

What is the difference between a CSS transition and animation?

A transition interpolates when a property changes between an old and new state, such as hover or a toggled class. A keyframe animation runs a named sequence that can contain multiple stops, repeat, reverse, pause, and begin without a new state change. Use the simpler transition when two states are enough.

Why is my CSS animation not working?

First match animation-name to the @keyframes name and inspect computed animation-duration, whose initial value is 0s. Then confirm the selector and media query match, the values can interpolate, and display or DOM removal is not cancelling the effect. Check delay, iteration count, play state, and reduced-motion rules in developer tools.

What does animation-fill-mode: forwards do?

It keeps the animation effect at the point where its final iteration ended after playback. Backwards applies the relevant starting keyframe during a positive delay, and both combines the two. Fill mode does not change the component's durable base state; encode a real open, closed, or selected state in ordinary CSS.

Which CSS properties are best for animation performance?

Start with transform and opacity, then measure on representative devices. Properties that trigger layout or paint are harder to keep smooth, but compositor behavior is not guaranteed by a property name alone. Use browser performance tools to inspect frames, layout, paint, and layers before adding will-change or rewriting a semantic size change.

How do I respect prefers-reduced-motion?

Make the static, fully usable state the default and place non-essential transform animation inside @media (prefers-reduced-motion: no-preference). Alternatively, replace motion deliberately inside the reduce query while retaining state feedback. A global 0.01ms reset is not complete accessibility work; also test pause controls and flash limits where WCAG applies.