CSS

CSS Animations & Transitions: From Basics to Advanced

css-animations-tutorial

A button that just appears? Fine. A button that smoothly fades in with a subtle scale? That's what makes users think "this app feels polished." The crazy part is that you don't need JavaScript for most animations. CSS handles it natively, and it runs on the GPU when you do it right.

I've shipped dozens of production sites where CSS animations replaced entire animation libraries. The performance difference is real, and the code is significantly simpler. Let me show you everything I've learned.

Transitions vs. Animations: Know the Difference

Before we dive into code, let's clear up the confusion that trips up most beginners.

Transitions animate between two states. You define the start state and end state, and the browser figures out the in-between frames. They're triggered by state changes — hover, focus, class toggles.

Animations (using @keyframes) give you full control over every step. You can define as many intermediate states as you want, loop them, reverse them, and start them automatically.

Here's the rule of thumb I use: if it's a simple A-to-B change (like a hover effect), use a transition. If it's multi-step, looping, or needs to run on page load, use an animation.

Transitions: The 80/20 of CSS Motion

Transitions will cover 80% of your animation needs. Here's the basic syntax:

.button {
  background: #3b82f6;
  transform: scale(1);
  transition: all 0.3s ease;
}

.button:hover {
  background: #2563eb;
  transform: scale(1.05);
}

That works, but transition: all is lazy. It tells the browser to watch every property for changes. Be specific about what you're transitioning:

.button {
  background: #3b82f6;
  transform: scale(1);
  transition: background 0.3s ease, transform 0.2s ease;
}

Timing Functions That Don't Look Robotic

The default ease timing function is fine for most cases. But linear looks mechanical, and ease-in on its own often feels sluggish. My favorite combinations:

/* Snappy entrance */
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);

/* Smooth slide */
transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);

/* Quick fade */
transition: opacity 0.15s ease-out;

The cubic-bezier(0.34, 1.56, 0.64, 1) one is gold — it gives a slight overshoot bounce that feels organic without being cartoonish. I use it on buttons, cards, and modal entrances constantly.

Pro tip: Use Chrome DevTools' cubic-bezier editor to visually tweak your timing curves. Open the Styles panel, click the timing function preview next to any transition, and drag the curve handles. Way faster than guessing values.

Transition Delays for Staggered Effects

Want a list of cards to animate in one after another? You don't need JavaScript. Use transition-delay with CSS custom properties:

.card {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.4s ease, transform 0.4s ease;
  transition-delay: calc(var(--index) * 0.1s);
}

.cards-visible .card {
  opacity: 1;
  transform: translateY(0);
}

Then in your HTML, set the index on each card:

<div class="card" style="--index: 0">First</div>
<div class="card" style="--index: 1">Second</div>
<div class="card" style="--index: 2">Third</div>

Add the cards-visible class with a tiny bit of JS (like an Intersection Observer), and you've got a staggered reveal effect that would make a motion designer proud.

Keyframe Animations: Full Control

When transitions aren't enough, @keyframes steps in. The syntax is straightforward — define named waypoints, then apply them:

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

.hero-title {
  animation: fadeInUp 0.6s ease-out forwards;
}

The forwards keyword is crucial here. Without it, the element snaps back to its pre-animation state after the animation ends. Always ask yourself: "what state should this element end in?" and set fill-mode accordingly.

Multi-Step Animations

Here's where keyframes shine over transitions. You can define as many intermediate steps as you want using percentages:

@keyframes pulse {
  0% {
    transform: scale(1);
    box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.5);
  }
  50% {
    transform: scale(1.05);
    box-shadow: 0 0 0 15px rgba(59, 130, 246, 0);
  }
  100% {
    transform: scale(1);
    box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
  }
}

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

This creates a pulsing glow effect that repeats forever. Great for notification badges or call-to-action buttons that need to draw attention.

A Practical Loading Spinner

Every app needs a loading spinner. Here's one that's smooth, lightweight, and requires zero JavaScript:

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

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

Eight lines of CSS. No SVGs, no libraries, no dependencies. And because we're only animating transform, it runs at 60fps without breaking a sweat.

Performance: The Properties That Matter

This is the section most CSS animation tutorials skip, and it's arguably the most important one.

Not all CSS properties are created equal when it comes to animation performance. The browser rendering pipeline has three stages that an animation might trigger: Layout, Paint, and Composite.

The takeaway? Only animate transform and opacity whenever possible. Everything else is a compromise.

/* Bad: animating width causes layout recalculation */
.sidebar {
  width: 0;
  transition: width 0.3s ease;
}
.sidebar.open {
  width: 300px;
}

/* Good: use transform instead */
.sidebar {
  transform: translateX(-100%);
  transition: transform 0.3s ease;
}
.sidebar.open {
  transform: translateX(0);
}

Both achieve the same visual result. But the second one is dramatically smoother, especially on mobile devices where CPU power is limited.

Testing animation performance: Open Chrome DevTools → Performance tab → check "Enable advanced rendering instrumentation." Record while your animation runs, and look for dropped frames (red bars). If you see them, you're probably animating a layout or paint property.

The will-change Property

You might see will-change recommended as a performance silver bullet. It tells the browser to prepare for an upcoming animation by promoting the element to its own compositor layer:

.card {
  will-change: transform;
}
.card:hover {
  transform: translateY(-4px);
}

But here's the catch — don't overuse it. Every element with will-change gets its own GPU layer, which consumes memory. Slap it on 50 elements and you might actually make performance worse. Use it only on elements that genuinely need it, and consider removing it after animations complete.

Respecting User Preferences

Some users experience motion sickness or have vestibular disorders. Always respect the prefers-reduced-motion media query:

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

This is one of those things I wish I'd known about earlier in my career. It takes 30 seconds to add and makes your site accessible to a significant number of users. There's no excuse to skip it.

Putting It All Together: A Card Hover Effect

Here's a complete, production-ready card hover effect that uses everything we've covered:

.card {
  background: white;
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),
              box-shadow 0.25s ease;
}

.card:hover {
  transform: translateY(-6px);
  box-shadow: 0 12px 40px rgba(0,0,0,0.12);
}

.card .icon {
  transition: transform 0.3s ease;
}

.card:hover .icon {
  transform: rotate(10deg) scale(1.1);
}

@media (prefers-reduced-motion: reduce) {
  .card,
  .card .icon {
    transition: none;
  }
}

The card lifts up on hover with a growing shadow (creating depth), and the icon inside does a subtle rotation. It respects reduced motion preferences. And it only animates transform and box-shadow — the shadow is a minor paint cost, but it's worth it for the visual effect.

Capture Your CSS Animations as Images

Want to showcase your animated components? Convert any HTML/CSS to a crisp PNG image for portfolios and documentation.

Try HTML to PNG Free →

Common Mistakes I See All the Time

Final Thoughts

CSS animations are deceptively simple on the surface — a transition here, a keyframe there. But the difference between "it moves" and "it feels right" comes down to understanding timing functions, respecting the rendering pipeline, and choosing the right tool (transition vs. animation) for the job.

Start simple. Nail your hover states with transitions first. Then level up to keyframe animations for loading states and page entrances. And always, always test on real devices — what feels smooth on your 144Hz monitor might stutter on a budget Android phone.

The best animation is the one users don't consciously notice but would immediately miss if it were gone.

Sachin Bhanushali
Written by

Sachin Bhanushali

Full-stack developer and creator of HTMLtoImages. Building free, privacy-first developer tools that run entirely in your browser.