Home / Blog / CSS Animations with @keyframes: From Beginner to Advanced
CSS Animations 2026-09-23 · 11 min read

CSS Animations with @keyframes: From Beginner to Advanced

Master CSS animations using @keyframes. Learn animation-name, duration, timing-function, iteration-count, direction, fill-mode, and more.

What Are CSS Animations?

CSS animations let you smoothly transition an element between multiple style states — all without a single line of JavaScript. The secret ingredient is the @keyframes at-rule, which defines the exact CSS styles an element should have at each point during the animation sequence. Combined with animation properties on the element itself, you get full control over timing, looping, direction, and more.

Before CSS animations became mainstream, developers relied on JavaScript setInterval loops or jQuery's .animate() to move elements around. CSS animations run on the browser's compositor thread (when animating transform or opacity), making them far smoother and less CPU-intensive than script-based alternatives.

The @keyframes At-Rule

A @keyframes rule describes an animation sequence. You give it a name and then define style snapshots at percentage milestones (or using from / to keywords).

/* Simplest possible keyframe: from → to */
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* Multi-stop keyframe */
@keyframes bounce {
  0%   { transform: translateY(0); }
  40%  { transform: translateY(-30px); }
  60%  { transform: translateY(-20px); }
  80%  { transform: translateY(-10px); }
  100% { transform: translateY(0); }
}

You can stack multiple properties at a single keyframe stop, and you can even share stops using a comma list:

@keyframes pulse {
  0%, 100% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(1.08);
    opacity: 0.85;
  }
}

Attaching an Animation to an Element

Once you've defined your keyframes, apply the animation using the animation shorthand or individual properties.

.box {
  /* Shorthand: name | duration | timing-function | delay | iteration-count | direction | fill-mode */
  animation: fadeIn 0.6s ease-out 0s 1 normal forwards;
}

/* Equivalent longhand */
.box {
  animation-name: fadeIn;
  animation-duration: 0.6s;
  animation-timing-function: ease-out;
  animation-delay: 0s;
  animation-iteration-count: 1;
  animation-direction: normal;
  animation-fill-mode: forwards;
}

Understanding Every Animation Property

animation-name

The animation-name property links the element to a @keyframes rule by its identifier. You can apply multiple animations by comma-separating names:

.element {
  animation-name: fadeIn, pulse;
  animation-duration: 0.4s, 2s;
}

animation-duration

Sets how long one cycle of the animation takes. Always provide a unit — s (seconds) or ms (milliseconds). Omitting the unit will break the animation silently.

animation-timing-function

Controls the pace of the animation between keyframes. Common values:

  • ease — slow start, fast middle, slow end (default)
  • linear — constant speed throughout
  • ease-in — starts slowly, accelerates
  • ease-out — starts fast, decelerates
  • ease-in-out — slow at both ends
  • cubic-bezier(x1, y1, x2, y2) — custom curve
  • steps(4, end) — discrete step animation (great for sprite sheets)
/* Custom spring-like bounce */
.spring {
  animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* Sprite sheet animation: 8 frames */
.sprite {
  animation-timing-function: steps(8, end);
}

animation-delay

Postpones the start of the animation. A negative delay makes the animation start mid-cycle, which is useful for staggered group animations:

.item:nth-child(1) { animation-delay: 0s; }
.item:nth-child(2) { animation-delay: 0.1s; }
.item:nth-child(3) { animation-delay: 0.2s; }
/* -0.5s would start 0.5 seconds INTO the animation cycle immediately */
.item { animation-delay: -0.5s; }

animation-iteration-count

How many times the animation runs. Use a number or infinite for a loop that never stops:

.spinner { animation-iteration-count: infinite; }
.flash   { animation-iteration-count: 3; }
.half    { animation-iteration-count: 0.5; } /* plays half a cycle */

animation-direction

Controls which direction the keyframes play:

  • normal — 0% → 100% every cycle (default)
  • reverse — 100% → 0% every cycle
  • alternate — first cycle forward, second backward, etc.
  • alternate-reverse — first cycle backward, then alternates
.pendulum {
  animation-name: swing;
  animation-duration: 1s;
  animation-direction: alternate;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
}

animation-fill-mode

animation-fill-mode is one of the most misunderstood properties. It determines what styles apply to the element before the animation starts and after it ends.

  • none — element reverts to original styles outside the animation (default)
  • forwards — element retains the final keyframe's styles after animation ends
  • backwards — element applies the first keyframe's styles during the delay period
  • both — applies rules of both forwards and backwards
/* Without forwards, the element will pop back to opacity:0 after fading in */
.fade-in {
  opacity: 0;
  animation: fadeIn 0.5s ease-out forwards;
}

animation-play-state

Toggle an animation on/off without removing it — perfect for pause buttons:

.spinner { animation: spin 1s linear infinite; }
.spinner.paused { animation-play-state: paused; }

Practical CSS Animation Examples

Loading Spinner

@keyframes spin {
  to { transform: rotate(360deg); }
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #e0e0e0;
  border-top-color: #3b82f6;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

Skeleton Screen Shimmer

@keyframes shimmer {
  from { background-position: -200% 0; }
  to   { background-position:  200% 0; }
}

.skeleton {
  background: linear-gradient(
    90deg,
    #f0f0f0 25%,
    #e0e0e0 50%,
    #f0f0f0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 6px;
  height: 16px;
}

Entrance Animation with Stagger

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

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

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

Triggering Animations with JavaScript

You can add and remove CSS classes to trigger animations dynamically. A common pattern is to remove a class, force a reflow, then re-add it:

function replay(element) {
  element.classList.remove('animate');
  // Force reflow so the browser registers the class removal
  void element.offsetWidth;
  element.classList.add('animate');
}

document.querySelector('.btn').addEventListener('click', () => {
  replay(document.querySelector('.box'));
});

Respecting User Preferences

Always wrap heavy animations in a prefers-reduced-motion media query. Many users experience motion sickness or have vestibular disorders — this is an accessibility requirement, not a nice-to-have:

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Debugging CSS Animations

Browser DevTools make animation debugging much easier. In Chrome DevTools, open the Animations panel (More Tools → Animations) to scrub through animations frame-by-frame, slow them down, and inspect timing curves. You can also use our Live HTML/CSS/JS Editor to prototype and iterate on animations in real time without any setup.

Pro Tip: Add animation-play-state: paused temporarily while building complex multi-step keyframes. It lets you inspect the element's style at the first keyframe before the animation runs away.

Multiple Animations on One Element

An element can run several @keyframes simultaneously. List them as comma-separated values in each animation property:

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

@keyframes glow {
  0%, 100% { box-shadow: 0 0 8px #3b82f6; }
  50%       { box-shadow: 0 0 24px #3b82f6, 0 0 48px #60a5fa; }
}

.orb {
  animation:
    float 3s ease-in-out infinite,
    glow  2s ease-in-out infinite;
}

CSS Animation vs CSS Transition

Transitions are for simple A→B state changes triggered by user interaction (hover, focus). Animations with @keyframes are for multi-step sequences that can loop, delay, and run automatically on page load. Use transitions for UI micro-interactions; use animations for looping effects, entrances, and complex sequences.

Performance Considerations

For best performance, stick to animating only transform and opacity. Animating properties like width, height, top, or padding triggers expensive layout recalculations. See our article on CSS animation performance for a deep dive. You can experiment with all these techniques in our Live HTML/CSS/JS Editor.

Frequently Asked Questions

What is the difference between CSS transitions and CSS animations?
Transitions animate between two states (A to B) when a property changes, usually triggered by user interaction like hover. CSS animations with @keyframes define multi-step sequences that can start automatically, loop, run in reverse, and have fine-grained timing control at every percentage step.
Why does my animation snap back to its original state after finishing?
You need to set animation-fill-mode: forwards (or both). Without it, the element reverts to its pre-animation styles once the animation cycle completes. The forwards value tells the browser to retain the final keyframe's styles after the animation ends.
How do I make a CSS animation loop forever?
Set animation-iteration-count: infinite. For example: animation: spin 1s linear infinite; will rotate an element continuously. Combine with animation-direction: alternate to make it ping-pong back and forth.
Can I animate multiple properties in one @keyframes rule?
Yes. Each keyframe stop can include any number of CSS properties. You can also apply multiple separate @keyframes animations to a single element by comma-separating the names and their respective durations and timings.
Is CSS animation bad for performance?
Not inherently — but animating the wrong properties is. Stick to transform and opacity, which are handled by the GPU compositor and never trigger layout or paint. Animating layout properties like width, height, or top causes expensive reflows on every frame.
How do I pause and resume a CSS animation with JavaScript?
Toggle the animation-play-state property between "running" and "paused" using JavaScript: element.style.animationPlayState = 'paused'. You can also achieve this with a CSS class that sets animation-play-state: paused.

Try Our Free HTML CSS Tools

Practice what you've learned with our free online tools — no installation required.

Live HTML/CSS Editor CSS Formatter CSS Minifier