CSS Animation Complete Guide: @keyframes, transition & Performance

Published April 2026 · CSS Tutorial · Try CSS Minifier →

CSS animations bring interfaces to life. From subtle hover effects to complex loading sequences, well-crafted animations improve user experience by providing visual feedback, guiding attention, and creating a sense of polish. CSS provides two complementary systems for animation: transition for simple state changes and @keyframes for complex multi-step animations.

This guide covers both systems in depth, along with performance optimization techniques that ensure your animations run at a smooth 60fps.

CSS Transitions: Animating State Changes

Transitions animate property changes between two states. They are triggered by state changes like :hover, :focus, class toggles, or JavaScript style changes.

Basic Transition Syntax

/* Transition a single property */
.button {
  background-color: #2563eb;
  transition: background-color 0.3s ease;
}

.button:hover {
  background-color: #1d4ed8;
}

/* Transition multiple properties */
.card {
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
}

/* Shorthand: property duration timing-function delay */
.button {
  transition: all 0.3s ease 0.1s;
}

/* Transition all animatable properties */
.link {
  transition: all 0.2s ease-in-out;
}

Timing Functions

The timing function controls the acceleration curve of the transition:

transition-timing-function: ease;        /* Default: slow start, fast middle, slow end */
transition-timing-function: linear;     /* Constant speed */
transition-timing-function: ease-in;    /* Slow start */
transition-timing-function: ease-out;   /* Slow end */
transition-timing-function: ease-in-out; /* Slow start and end */

/* Custom cubic-bezier curve */
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55); /* Bounce effect */

The cubic-bezier() function lets you define any custom easing curve. Use tools like cubic-bezier.com to visualize and create curves.

@keyframes: Multi-Step Animations

While transitions handle two-state changes, @keyframes define animations with multiple steps, giving you full control over every frame.

Basic @keyframes Syntax

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* Using percentage keyframes */
@keyframes slideInUp {
  0% {
    opacity: 0;
    transform: translateY(30px);
  }
  50% {
    opacity: 0.8;
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

Applying Animations

/* Individual properties */
.element {
  animation-name: slideInUp;
  animation-duration: 0.6s;
  animation-timing-function: ease-out;
  animation-delay: 0.2s;
  animation-iteration-count: 1;
  animation-direction: normal;
  animation-fill-mode: forwards;
  animation-play-state: running;
}

/* Shorthand (recommended) */
.element {
  animation: slideInUp 0.6s ease-out 0.2s 1 normal forwards;
}

Animation Properties Explained

Practical Animation Patterns

Loading Spinner

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

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

Pulse Effect

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

.notification {
  animation: pulse 2s ease-in-out infinite;
}

Slide-In Navigation

@keyframes slideInLeft {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}

.nav-open .nav-panel {
  animation: slideInLeft 0.3s ease-out forwards;
}

Staggered Card Entrance

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

.card {
  opacity: 0;
  animation: fadeInUp 0.5s ease-out forwards;
}

.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
.card:nth-child(4) { animation-delay: 0.4s; }

Typing Effect with Steps

@keyframes typing {
  from { width: 0; }
  to { width: 100%; }
}

@keyframes blink {
  50% { border-color: transparent; }
}

.typewriter {
  overflow: hidden;
  white-space: nowrap;
  border-right: 2px solid #333;
  animation: typing 3s steps(20) forwards, blink 0.8s step-end infinite;
}

Performance Optimization

Performance is critical for animations. A janky 15fps animation is worse than no animation at all. Understanding how the browser renders animations is key to keeping them smooth.

The Golden Rule: Animate Only transform and opacity

The browser rendering pipeline has four stages: Style → Layout → Paint → Composite. Animating transform and opacity skips the Layout and Paint stages, running entirely on the GPU compositor thread. This is the single most important performance rule.

/* BAD — triggers layout recalculation */
.bad {
  animation: moveBad 1s ease-in-out;
}

@keyframes moveBad {
  from { left: 0; }
  to { left: 100px; }
}

/* GOOD — runs on compositor thread only */
.good {
  animation: moveGood 1s ease-in-out;
}

@keyframes moveGood {
  from { transform: translateX(0); }
  to { transform: translateX(100px); }
}

Properties to avoid animating: width, height, top, left, margin, padding, border-width. These trigger layout recalculations.

will-change

The will-change property hints to the browser that an element will be animated, allowing it to prepare optimizations in advance.

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

Use will-change sparingly. Do not apply it to many elements at once — it consumes memory. Remove it after the animation completes if possible.

Use transform Instead of Width/Height

/* BAD — triggers layout */
.accordion-content {
  height: 0;
  transition: height 0.3s ease;
}
.accordion-content.open {
  height: 300px;
}

/* BETTER — use scaleY (approximation) */
.accordion-content {
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 0.3s ease;
}
.accordion-content.open {
  transform: scaleY(1);
}

Accessibility: prefers-reduced-motion

Always respect the user's motion preferences. Some users experience motion sickness or have vestibular disorders.

/* Default: animations enabled */
@media (prefers-reduced-motion: no-preference) {
  .animated {
    animation: fadeInUp 0.5s ease-out;
  }
}

/* Reduced motion: disable or simplify */
@media (prefers-reduced-motion: reduce) {
  .animated {
    animation: none;
    opacity: 1;
    transform: none;
  }

  .transition-element {
    transition: none;
  }
}

A simpler approach is to define animations by default and disable them for users who prefer reduced motion:

* {
  animation-duration: 0.01ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.01ms !important;
}

@media (prefers-reduced-motion: no-preference) {
  * {
    animation-duration: revert !important;
    transition-duration: revert !important;
  }
}

JavaScript Integration

CSS animations can be controlled with JavaScript for more dynamic behavior:

const element = document.querySelector('.element');

// Listen for animation events
element.addEventListener('animationstart', () => console.log('Started'));
element.addEventListener('animationend', () => console.log('Finished'));
element.addEventListener('animationiteration', () => console.log('Looped'));

// Pause and resume
element.style.animationPlayState = 'paused';
element.style.animationPlayState = 'running';

// Detect when transition ends
element.addEventListener('transitionend', (e) => {
  if (e.propertyName === 'transform') {
    console.log('Transform transition complete');
  }
});

Frequently Asked Questions

What CSS properties can be animated?

Most numeric CSS properties can be animated: transform, opacity, color, width, height, margin, padding, font-size, box-shadow, and more. For best performance, stick to transform and opacity.

What is the difference between transition and animation?

Transitions animate between two states triggered by a change (like hover). Animations run independently with multiple keyframes and can loop, pause, and reverse without a state change trigger.

How do I make CSS animations smoother?

Animate only transform and opacity (GPU-accelerated), use will-change sparingly, avoid animating layout properties like width/height/margin, and test on lower-end devices.

Can I pause a CSS animation?

Yes, use animation-play-state: paused to pause and animation-play-state: running to resume. You can toggle this with JavaScript or CSS hover states.

How do I respect prefers-reduced-motion?

Wrap animations in @media (prefers-reduced-motion: no-preference) or disable them with @media (prefers-reduced-motion: reduce) { animation: none; }. Always provide this accessibility consideration.