CSS Animations Demystified โ€”
Mastering transition and @keyframes

A button that subtly shifts color on hover. A spinner that keeps turning. Content that fades into view as the page loads. All of this is pure CSS โ€” no JavaScript required. Let's learn how it works.

transition vs animation โ€” Two Tools, Different Jobs

CSS gives you two mechanisms for motion: transition and animation (with @keyframes). They look similar in tutorials, but they serve fundamentally different purposes in real projects.

transition smooths the change between two states. You define the starting state and the ending state, and the browser fills in the frames in between. Critically, a transition needs a trigger โ€” a :hover, a :focus, or a class change via JavaScript. Without a trigger, nothing happens.

animation runs on its own. It can start on page load, loop forever, go in reverse, and pass through as many intermediate stages as you want. If you need something to move without the user doing anything, this is your tool.

transition โ€” smooth state change on trigger
.button {
  background: #7c3aed;
  transition: background 0.3s ease;
}
.button:hover {
  background: #5b21b6;
}
animation โ€” self-running motion
@keyframes spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
.spinner {
  animation: spin 1s linear infinite;
}
โ–ถ Live Demo โ€” transition vs animation side by side
transition (hover me)
animation (auto-playing)
๐Ÿ“Œ Key Point
A good rule of thumb: if the motion responds to user interaction, use transition. If it plays on its own, use animation. Getting this distinction right from the start keeps your CSS clean and predictable.

Mastering transition โ€” the Four Sub-Properties

The transition shorthand actually bundles four sub-properties: transition-property (what changes), transition-duration (how long), transition-timing-function (what curve), and transition-delay (when it starts). In practice, you'll write them as a single line most of the time.

The transition shorthand
/* transition: [property] [duration] [timing] [delay] */

.card {
  transition: transform 0.3s ease;
}

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

Easing โ€” the "Feel" of Motion

The timing function is what separates animations that feel polished from those that feel robotic. Even at the same duration, ease and linear create completely different impressions. The human eye expects acceleration and deceleration โ€” that's why ease and ease-in-out tend to feel more natural.

โ–ถ Live Demo โ€” Easing comparison (hover to play)
linear
ease
ease-in-out
bouncy

Hover anywhere on the demo to trigger all balls simultaneously

๐Ÿ’ก Pro Tip
For most UI interactions, ease or ease-in-out works perfectly. Use linear for things that should feel mechanical โ€” progress bars, rotations, infinite scrollers. For custom personality, try cubic-bezier() โ€” the visual editor at cubic-bezier.com is your best friend.

The Hover Lift โ€” the Pattern You'll Use Everywhere

The "lift on hover" effect is probably the single most common transition pattern in production websites. Combine transform: translateY() with a deepening box-shadow, and the card appears to float off the page.

Hover lift card CSS
.card {
  transition: transform 0.3s ease,
              box-shadow 0.3s ease;
}
.card:hover {
  transform: translateY(-6px);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
}
โ–ถ Live Demo โ€” Hover lift cards
๐ŸŽจ
Design
๐Ÿ’ป
Code
๐Ÿš€
Deploy

@keyframes โ€” Writing Your Own Animation Script

While transitions handle A-to-B changes, @keyframes lets you choreograph multi-step sequences. You define keyframes at percentage points from 0% to 100%, and the browser interpolates between them. It's like writing a script for a play โ€” you set the key moments and the browser fills in the rest.

@keyframes basic syntax
@keyframes fadeInUp {
  0% {
    opacity: 0;
    transform: translateY(30px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

.element {
  animation: fadeInUp 0.6s ease forwards;
}

Four Patterns You'll Reach For Again and Again

โ–ถ Live Demo โ€” Essential animation patterns
spin
pulse
bounce
shake
The @keyframes behind the four patterns
/* Spin */
@keyframes spin {
  to { transform: rotate(360deg); }
}

/* Pulse */
@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50%       { transform: scale(1.15); }
}

/* Bounce */
@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  50%       { transform: translateY(-16px); }
}

/* Shake */
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25%       { transform: translateX(-6px); }
  75%       { transform: translateX(6px); }
}

Building Loading Spinners with Pure CSS

Loading spinners are one of the most common real-world uses for CSS animations. You don't need a library โ€” a few lines of CSS and a single HTML element can produce something polished and professional.

โ–ถ Live Demo โ€” Three spinner styles
ring
dual ring
dots
๐Ÿ’ก Pro Tip
The staggered animation-delay technique in the dot spinner is incredibly versatile. Any time you have multiple identical elements and want a "wave" effect โ€” typing indicators, cascading reveals, equalizer bars โ€” just offset the delay on each child by a small increment.

Accessibility โ€” Respecting prefers-reduced-motion

Animations make interfaces feel alive, but not everyone experiences them the same way. People with vestibular disorders can experience dizziness, nausea, or disorientation from excessive motion on screen. This isn't an edge case โ€” it affects a significant number of users.

CSS provides the prefers-reduced-motion media query, which detects when a user has enabled the "reduce motion" setting in their OS. Respecting this setting isn't just a nice thing to do โ€” it's the right way to build.

โœ… The recommended approach
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
โŒ The problematic approach
/* animation: none removes fill-mode state too โ€” can break layouts */
@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; }
}
โš ๏ธ Watch Out
Using animation-duration: 0.01ms instead of animation: none is a deliberate choice. If your animation uses fill-mode: forwards to persist its end state (like a fade-in that should remain visible), animation: none would remove that final state entirely. The near-zero duration lets the animation complete instantly, preserving the end state without any visible motion.

Takeaways

  • Use transition for state changes triggered by user interaction (hover, focus, class toggle) and animation with @keyframes for motion that plays automatically or loops.
  • The timing function (easing) defines how motion feels โ€” ease and ease-in-out for natural UI motion, linear for mechanical movement, cubic-bezier() for brand personality.
  • The hover lift pattern (translateY + box-shadow transition) is the most commonly used effect in production and takes just a few lines of CSS.
  • Loading spinners built with pure CSS require no external libraries โ€” a border, border-radius, and a rotate keyframe animation are all you need for a professional result.
  • Stagger animation-delay across sibling elements to create wave, cascade, and typing-indicator effects with minimal code.
  • Always respect prefers-reduced-motion โ€” set animation-duration to 0.01ms (not animation: none) to preserve fill-mode states while eliminating visible motion.