Skip to content
C Codeloom
HTML & CSS

CSS Animations and Keyframes Explained

How CSS keyframe animations actually work: timing functions, fill modes, composition, and the patterns that keep them smooth and predictable.

·5 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • How @keyframes and animation properties connect
  • What fill-mode and direction do
  • Which properties animate cheaply on the GPU
  • How to chain and stagger animations
  • How to avoid jank and reflows

Prerequisites

  • Comfortable with HTML and CSS

CSS animations are deceptively simple at first glance and surprisingly deep in practice. You write @keyframes, attach an animation shorthand, and the browser does the rest. But the difference between an animation that feels delightful and one that feels broken often comes down to which properties you animate and how you compose multiple animations together.

What animations are and why they matter

A CSS animation interpolates property values over time according to a set of keyframes. Unlike transitions, which only run in response to a state change, animations run on their own schedule. You can loop them, chain them, pause them, and orchestrate them entirely from the stylesheet.

Animations matter because motion communicates. A subtle slide tells the user where something came from. A pulse draws the eye to a call to action. Done well, motion makes interfaces feel alive. Done poorly, it makes them feel slow and broken.

Mental model

Think of an animation as a function from time to property values. @keyframes defines the function. The animation shorthand attaches the function to an element and specifies how long to run, when to start, how to ease, and what to do when it ends.


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

animation:  slide-in   0.3s   ease-out   0.1s   forwards;
            |          |      |          |      |
            name       dur    easing     delay  fill-mode
The pieces of a CSS animation and what each one controls

Fill-mode is the most misunderstood part. Without fill-mode: forwards, the element snaps back to its pre-animation state after finishing. With forwards, it keeps the final keyframe values. With backwards, it adopts the first keyframe values during the delay.

Hands-on example

Let us build a small toast notification that slides up, holds, and slides down. We will use two animations chained together.

.toast {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  padding: 0.75rem 1rem;
  background: #111;
  color: white;
  border-radius: 0.5rem;
  opacity: 0;
  animation: toast-in 0.25s ease-out 0s forwards,
             toast-out 0.25s ease-in 2.5s forwards;
}

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

@keyframes toast-out {
  to { opacity: 0; transform: translateY(20px); }
}

Two animations run on the same element with different delays. The first one rises and fades in over 0.25 seconds. The second one waits 2.5 seconds, then fades out. Both use forwards so the toast actually stays in its final state. Without forwards on the out animation, the toast would suddenly reappear at full opacity when the animation ends.


time  0s        0.25s             2.5s            2.75s
      |           |                 |                |
in    [ ===>===> ]
hold              [================ ]
out                                 [ ===>===> ]
Timeline of the toast: in, hold, out

Notice we animated transform and opacity, not top or height. That is intentional. The browser compositor can move and fade an element on the GPU without re-laying out the page. Animating top, width, or margin forces a layout pass on every frame, and on a busy page that means dropped frames.

Common pitfalls

The first is animating expensive properties. width, height, top, left, margin, and padding all trigger layout. On a simple page that might be fine, but on a complex one it stutters. transform and opacity are the safe defaults.

The second is forgetting fill-mode. The element finishes the animation, then snaps back. Developers add a setTimeout to set a class and re-implement what fill-mode: forwards already does for free.

The third is competing transforms. If you animate transform: translateY in one keyframe and also have transform: scale on hover, the two fight each other. CSS does not compose them automatically. You have to combine them in the same property: transform: translateY(0) scale(1.05).

A fourth is ignoring prefers-reduced-motion. Some users actively disable motion due to vestibular issues. Wrap non-essential animations in a media query that respects their preference.

@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; opacity: 1; }
}

Best practices

Keep durations small. UI motion in the 150 to 300 millisecond range feels responsive. Longer animations should be reserved for meaningful transitions where the duration carries information.

Use easing curves that match the motion. Ease-out for entrances (fast then slow, like something settling into place). Ease-in for exits (slow then fast, like something leaving). Cubic-bezier for custom personality.

Compose with delays instead of nesting. If you want a list of items to stagger in, give each item the same animation with a different delay. CSS variables make this easy.

.item { animation: slide-in 0.3s ease-out var(--d) forwards; }
.item:nth-child(1) { --d: 0ms; }
.item:nth-child(2) { --d: 60ms; }
.item:nth-child(3) { --d: 120ms; }

Profile in dev tools. The Performance panel shows you which frames dropped and why. If you see purple bars (layout) during your animation, you are animating the wrong property.

Wrap-up

CSS animations are a small surface area with a lot of leverage. The key ideas are: keyframes define a function over time, the animation shorthand attaches it, fill-mode preserves end state, and only transform and opacity animate cheaply. Stay inside those rails and your motion will be smooth on a laptop, smooth on a phone, and respectful of users who prefer no motion at all. That combination is what separates polished interfaces from twitchy ones.