CSS animations let you transition an element between multiple states over time — without JavaScript. They are smoother than JS-driven animations for visual effects, better for performance when done correctly, and supported in every modern browser.

This guide covers how CSS animations work from the ground up: the @keyframes rule, every animation property, timing functions, common ready-to-use patterns, and the performance rules you must follow.

How CSS animation works

A CSS animation has two parts that work together:

/* Step 1: Define the keyframes */
@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Step 2: Apply to an element */
.modal {
  animation: fadeIn 0.3s ease forwards;
}

@keyframes syntax

Inside @keyframes, you use percentage values to define the state of the element at each point in the animation. from is an alias for 0% and to is an alias for 100%.

@keyframes slideUp {
  0%   { transform: translateY(20px); opacity: 0; }
  100% { transform: translateY(0);    opacity: 1; }
}

/* Multiple stops */
@keyframes bounce {
  0%   { transform: translateY(0); }
  30%  { transform: translateY(-20px); }
  60%  { transform: translateY(-10px); }
  80%  { transform: translateY(-4px); }
  100% { transform: translateY(0); }
}

/* Same styles at multiple stops */
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.4; }
}

All animation properties

PropertyWhat it doesExample value
animation-nameWhich @keyframes to usefadeIn
animation-durationHow long one cycle takes0.3s
animation-timing-functionSpeed curve of the animationease, linear
animation-delayWait before starting0.2s
animation-iteration-countHow many times to repeat1, infinite
animation-directionForward, reverse, or alternatenormal, alternate
animation-fill-modeState before/after animation runsforwards, both
animation-play-statePause or run the animationrunning, paused

The shorthand

All properties can be written in one line. The order that matters: duration must come before delay.

/* name | duration | easing | delay | iterations | direction | fill-mode */
animation: slideUp 0.4s ease-out 0.1s 1 normal forwards;

/* Minimal — name and duration are required */
animation: fadeIn 0.3s;

/* Multiple animations separated by commas */
animation: fadeIn 0.3s ease, slideUp 0.4s ease-out;

Timing functions explained

The timing function controls how the animation accelerates and decelerates through its duration.

ValueBehaviourBest for
easeSlow start, fast middle, slow endMost UI animations — default
linearConstant speed throughoutSpinning loaders, progress bars
ease-inSlow start, fast endElements leaving the screen
ease-outFast start, slow endElements entering the screen
ease-in-outSlow start and endRepositioning elements
cubic-bezier()Custom curve with 4 control pointsBrand-specific motion feel
steps(n)Jumps in discrete stepsSprite animations, typewriter effect
/* Custom cubic-bezier — use cubic-bezier.com to generate */
animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* springy overshoot */

/* Steps — typewriter effect */
@keyframes typing {
  from { width: 0; }
  to   { width: 100%; }
}
.typewriter {
  overflow: hidden;
  white-space: nowrap;
  animation: typing 2s steps(30) forwards;
}

fill-mode — the most misunderstood property

By default, an element snaps back to its original style when an animation ends. animation-fill-mode controls what happens before and after.

/* none (default) — element returns to original state after animation */
animation-fill-mode: none;

/* forwards — element keeps the final keyframe state */
animation-fill-mode: forwards;

/* backwards — applies the first keyframe during the delay period */
animation-fill-mode: backwards;

/* both — applies backwards before and forwards after */
animation-fill-mode: both;

Most of the time you want forwards or both. Using none causes a visible snap-back at the end of the animation, which looks broken on entrance effects like fade-in or slide-up.

Common ready-to-use patterns

Fade in on load

@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.page-content {
  animation: fadeIn 0.4s ease both;
}

Slide up entrance

@keyframes slideUp {
  from { transform: translateY(16px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}
.card {
  animation: slideUp 0.35s ease-out both;
}

Infinite spinning loader

@keyframes spin {
  to { transform: rotate(360deg); }
}
.spinner {
  width: 24px;
  height: 24px;
  border: 3px solid #2d3148;
  border-top-color: #7c6af7;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

Pulsing skeleton loader

@keyframes shimmer {
  0%   { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}
.skeleton {
  background: linear-gradient(90deg, #1a1d27 25%, #2d3148 50%, #1a1d27 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 4px;
}

Staggered list entrance

@keyframes fadeSlide {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
.list-item {
  animation: fadeSlide 0.3s ease both;
}
/* Delay each item progressively */
.list-item:nth-child(1) { animation-delay: 0s; }
.list-item:nth-child(2) { animation-delay: 0.05s; }
.list-item:nth-child(3) { animation-delay: 0.1s; }
.list-item:nth-child(4) { animation-delay: 0.15s; }

Performance — what you can and cannot animate

Not all CSS properties are equal when it comes to animation performance. The browser has to do very different amounts of work depending on what you change.

PropertyPerformanceWhy
transform✅ ExcellentHandled by GPU — no layout recalculation
opacity✅ ExcellentGPU compositing only
filter🟡 GoodGPU but heavier than transform/opacity
color / background🟡 AcceptableTriggers repaint but not layout
width / height🔴 AvoidTriggers full layout recalculation
top / left / margin🔴 AvoidTriggers layout — use transform instead

Rule: Animate only transform and opacity whenever possible. To move an element, use transform: translate() — never animate top, left, or margin.

will-change

For complex animations, you can hint to the browser that an element will be animated so it can prepare ahead of time:

.animated-element {
  will-change: transform, opacity;
}

/* Remove it after animation completes to free GPU memory */
.animated-element.done {
  will-change: auto;
}

Do not overuse will-change. Applying it to many elements at once consumes extra GPU memory and can hurt performance rather than help it. Only use it on elements that are about to animate.

Accessibility — respecting user preferences

Some users have vestibular disorders or motion sensitivity and set their operating system to reduce motion. Always respect this preference using the prefers-reduced-motion media query.

@keyframes slideUp {
  from { transform: translateY(16px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}

.card { animation: slideUp 0.35s ease-out both; }

/* Disable or simplify animations for users who prefer it */
@media (prefers-reduced-motion: reduce) {
  .card {
    animation: fadeIn 0.1s ease both; /* simple fade instead of motion */
  }
}

Generate CSS animations visually

Our CSS Animation Generator lets you build keyframe animations in real time and copy the exact code — no manual writing needed.

Open Animation Generator →