CSS Transitions

โ–ถ Try It Yourself

CSS transitions are the simplest way to add motion to a webpage โ€” they smoothly animate a property from one value to another whenever that property changes. A single transition declaration turns an instant state switch into a fluid, polished interaction. In this lesson you will learn all four transition properties, master timing functions, and understand exactly which CSS properties are animatable.

The Four Transition Properties

Property Controls Example Value
transition-property Which CSS property to animate background-color, transform, all
transition-duration How long the animation takes 0.3s, 200ms
transition-timing-function Acceleration curve โ€” how the value changes over time ease, linear, cubic-bezier()
transition-delay Wait before the animation starts 0s, 0.1s
transition Shorthand: property duration timing-function delay background-color 0.3s ease 0s

Built-in Timing Functions

Keyword cubic-bezier Equivalent Feel
ease cubic-bezier(0.25, 0.1, 0.25, 1) Default โ€” fast start, gentle deceleration
linear cubic-bezier(0, 0, 1, 1) Constant speed โ€” mechanical, robotic feel
ease-in cubic-bezier(0.42, 0, 1, 1) Slow start, fast end โ€” feels like launching
ease-out cubic-bezier(0, 0, 0.58, 1) Fast start, gentle landing โ€” natural and common
ease-in-out cubic-bezier(0.42, 0, 0.58, 1) Slow start and end โ€” symmetric, refined
cubic-bezier(x1,y1,x2,y2) Custom curve Full control โ€” use tools like easings.net

Animatable vs Non-Animatable Properties

Animatable โœ… Not Animatable โŒ Notes
opacity, transform display display cannot transition between none and block
color, background-color font-family font-family changes are instant
width, height, padding, margin float, position Positional layout modes cannot interpolate
border-radius, box-shadow content Generated content is non-animatable
font-size, line-height background-image (in most cases) Gradient to gradient can transition in modern browsers
Note: Always prefer transitioning transform and opacity over width, height, top, or left. Transforms and opacity are composited on the GPU and do not trigger layout recalculation โ€” they run at 60fps even on complex pages. Transitioning layout properties like width forces the browser to recompute the entire layout on every frame, causing jank.
Tip: To animate multiple properties with different durations, separate them with commas: transition: transform 0.3s ease, opacity 0.2s ease, box-shadow 0.3s ease. Each property gets its own duration and easing โ€” far more precise than transition: all 0.3s, which applies the same timing to every property change including unexpected ones.
Warning: Avoid transition: all in production code. It transitions every property that changes โ€” including width, height, and color during JavaScript-driven class changes โ€” often producing unintended animations. Always specify exactly which properties should transition.

Basic Example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
  <title>CSS Transitions</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    html { font-size: 100%; }
    body { font-family: 'Inter', system-ui, sans-serif; background: #f8fafc; padding: 48px 32px; }
    h3   { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #64748b; margin: 40px 0 16px; }
    .row { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; margin-bottom: 8px; }

    /* โ”€โ”€ Button transitions โ”€โ”€ */
    .btn {
      padding: 12px 24px;
      border: none;
      border-radius: 10px;
      font-size: 0.875rem;
      font-weight: 600;
      cursor: pointer;
      font-family: inherit;
    }

    /* Colour transition */
    .btn-color {
      background: #4f46e5;
      color: white;
      transition: background-color 0.25s ease;
    }
    .btn-color:hover { background: #4338ca; }

    /* Transform + shadow transition */
    .btn-lift {
      background: white;
      color: #0f172a;
      border: 1.5px solid #e2e8f0;
      box-shadow: 0 1px 3px rgba(0,0,0,0.08);
      transition: transform 0.2s ease, box-shadow 0.2s ease;
    }
    .btn-lift:hover {
      transform: translateY(-3px);
      box-shadow: 0 8px 24px rgba(0,0,0,0.12);
    }

    /* Multiple properties */
    .btn-multi {
      background: #0f172a;
      color: white;
      transition: background-color 0.3s ease,
                  transform 0.2s ease,
                  letter-spacing 0.3s ease;
    }
    .btn-multi:hover {
      background: #4f46e5;
      transform: scale(1.04);
      letter-spacing: 0.05em;
    }

    /* Timing function comparison */
    .timing-demo {
      position: relative;
      height: 60px;
      background: #f1f5f9;
      border-radius: 10px;
      overflow: hidden;
      margin-bottom: 8px;
      max-width: 600px;
    }
    .timing-ball {
      position: absolute;
      top: 50%;
      left: 8px;
      transform: translateY(-50%);
      width: 44px; height: 44px;
      border-radius: 50%;
      display: flex; align-items: center; justify-content: center;
      font-size: 0.65rem; font-weight: 700; color: white;
      transition-property: left;
      transition-duration: 1.2s;
    }
    .timing-demo:hover .timing-ball { left: calc(100% - 52px); }

    .ball-ease        { background: #4f46e5; transition-timing-function: ease; }
    .ball-linear      { background: #0891b2; transition-timing-function: linear; }
    .ball-ease-in     { background: #7c3aed; transition-timing-function: ease-in; }
    .ball-ease-out    { background: #10b981; transition-timing-function: ease-out; }
    .ball-ease-in-out { background: #f59e0b; transition-timing-function: ease-in-out; }

    /* Card hover โ€” multiple property transition */
    .card-demo {
      width: 220px;
      background: white;
      border-radius: 14px;
      border: 1.5px solid #e2e8f0;
      padding: 24px;
      cursor: pointer;
      transition: transform 0.25s ease,
                  box-shadow 0.25s ease,
                  border-color 0.25s ease;
    }
    .card-demo:hover {
      transform: translateY(-6px);
      box-shadow: 0 20px 40px rgba(79,70,229,0.12);
      border-color: #a5b4fc;
    }
    .card-demo-title { font-size: 0.95rem; font-weight: 700; color: #0f172a; margin-bottom: 6px; }
    .card-demo-text  { font-size: 0.8rem; color: #64748b; line-height: 1.5; }

    /* Transition with delay โ€” staggered */
    .stagger-row { display: flex; gap: 10px; }
    .stagger-item {
      width: 48px; height: 48px;
      background: #4f46e5;
      border-radius: 10px;
      transition: transform 0.3s ease, opacity 0.3s ease;
    }
    .stagger-item:nth-child(1) { transition-delay: 0s; }
    .stagger-item:nth-child(2) { transition-delay: 0.06s; }
    .stagger-item:nth-child(3) { transition-delay: 0.12s; }
    .stagger-item:nth-child(4) { transition-delay: 0.18s; }
    .stagger-item:nth-child(5) { transition-delay: 0.24s; }
    .stagger-row:hover .stagger-item {
      transform: translateY(-12px);
      opacity: 0.7;
    }
  </style>
</head>
<body>

  <h3>Button Transitions โ€” hover each</h3>
  <div class="row">
    <button class="btn btn-color">Colour fade</button>
    <button class="btn btn-lift">Lift + shadow</button>
    <button class="btn btn-multi">Multi-property</button>
  </div>

  <h3>Timing Functions โ€” hover the track to race the balls</h3>
  <div style="display:grid;gap:8px;max-width:600px">
    <div class="timing-demo"><div class="timing-ball ball-ease">ease</div></div>
    <div class="timing-demo"><div class="timing-ball ball-linear">linear</div></div>
    <div class="timing-demo"><div class="timing-ball ball-ease-in">in</div></div>
    <div class="timing-demo"><div class="timing-ball ball-ease-out">out</div></div>
    <div class="timing-demo"><div class="timing-ball ball-ease-in-out">in-out</div></div>
  </div>

  <h3>Card Hover โ€” transform + shadow + border</h3>
  <div class="row">
    <div class="card-demo">
      <p class="card-demo-title">Hover me</p>
      <p class="card-demo-text">Three properties transition independently with the same duration and easing.</p>
    </div>
  </div>

  <h3>Staggered Delay โ€” hover the row</h3>
  <div class="stagger-row">
    <div class="stagger-item"></div>
    <div class="stagger-item"></div>
    <div class="stagger-item"></div>
    <div class="stagger-item"></div>
    <div class="stagger-item"></div>
  </div>

</body>
</html>

How It Works

Step 1 โ€” Transition Lives on the Base State

The transition property is always declared on the element’s default state, not on :hover. This means the transition plays both ways โ€” when entering the hover state and when leaving it. If you put transition only on :hover, the animation plays when hovering but snaps back instantly when you move the cursor away.

Step 2 โ€” GPU-Composited Properties Stay at 60fps

transform: translateY(-3px) moves the element visually without affecting the document layout. The browser hands this to the GPU compositor, which can animate it every frame without recalculating widths, heights, or positions of other elements. The result is butter-smooth motion even on content-heavy pages.

Step 3 โ€” Multiple Transitions with Comma Separation

The .btn-multi button transitions background-color at 0.3s, transform at 0.2s, and letter-spacing at 0.3s โ€” each with its own duration. The background and letter-spacing complete together while the scale completes slightly earlier, creating a layered, non-mechanical feel.

Step 4 โ€” transition-delay Creates Stagger

Each .stagger-item has the same transition but a different transition-delay โ€” 0s, 0.06s, 0.12s, 0.18s, 0.24s. When the parent is hovered, the items animate in sequence with a 60ms gap between each, creating a wave effect from a few lines of CSS and no JavaScript.

Step 5 โ€” ease-out for Entering, ease-in for Exiting

Natural motion decelerates as it arrives โ€” use ease-out for elements that appear or arrive (modals, dropdowns, tooltips). For elements that leave, ease-in (slow start, fast exit) feels more natural. For hover states that play in both directions, ease is a good compromise.

Real-World Example: Navigation with Animated Underline Indicator

/* animated-nav.css */
.nav { display: flex; gap: 4px; padding: 0 8px; }

.nav-item {
  position: relative;
  padding: 10px 16px;
  font-size: 0.875rem;
  font-weight: 500;
  color: #64748b;
  text-decoration: none;
  border-radius: 8px;
  transition: color 0.2s ease, background-color 0.2s ease;
}

/* Animated underline via pseudo-element */
.nav-item::after {
  content: '';
  position: absolute;
  bottom: 4px;
  left: 16px;
  right: 16px;
  height: 2px;
  background: #4f46e5;
  border-radius: 2px;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.25s ease-out;
}

.nav-item:hover { color: #0f172a; background: #f8fafc; }
.nav-item:hover::after { transform: scaleX(1); }
.nav-item.active { color: #4f46e5; font-weight: 600; }
.nav-item.active::after { transform: scaleX(1); }

/* โ”€โ”€ Animated toggle switch โ”€โ”€ */
.toggle {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
}
.toggle-track {
  width: 44px; height: 24px;
  background: #cbd5e1;
  border-radius: 9999px;
  position: relative;
  transition: background-color 0.25s ease;
}
.toggle-thumb {
  position: absolute;
  top: 3px; left: 3px;
  width: 18px; height: 18px;
  background: white;
  border-radius: 50%;
  box-shadow: 0 1px 4px rgba(0,0,0,0.2);
  transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
input[type="checkbox"]:checked + .toggle-track { background: #4f46e5; }
input[type="checkbox"]:checked + .toggle-track .toggle-thumb {
  transform: translateX(20px);
}
input[type="checkbox"] { position: absolute; opacity: 0; pointer-events: none; }

/* โ”€โ”€ Animated dropdown reveal โ”€โ”€ */
.dropdown-menu {
  background: white;
  border: 1px solid #e2e8f0;
  border-radius: 12px;
  padding: 8px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.08);
  opacity: 0;
  transform: translateY(-8px) scale(0.97);
  pointer-events: none;
  transition: opacity 0.2s ease-out,
              transform 0.2s ease-out;
}
.dropdown-trigger:focus-within .dropdown-menu,
.dropdown-trigger:hover .dropdown-menu {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
}

Common Mistakes

Mistake 1 โ€” Transitioning layout properties

โŒ Wrong โ€” width/height trigger expensive layout recalculations on every frame:

.box { width: 100px; transition: width 0.3s ease; }
.box:hover { width: 200px; } /* forces full layout reflow each frame */

โœ… Correct โ€” use transform: scaleX() for visual resize without layout cost:

.box { transform: scaleX(1); transform-origin: left; transition: transform 0.3s ease; }
.box:hover { transform: scaleX(2); } /* GPU-composited โ€” no layout thrash */

Mistake 2 โ€” Putting transition only on :hover

โŒ Wrong โ€” the exit animation snaps instantly when hover ends:

.btn:hover { background: #4f46e5; transition: background 0.3s ease; }
/* On mouse-out: background snaps back with no transition */

โœ… Correct โ€” transition on the base element applies in both directions:

.btn { background: #6366f1; transition: background-color 0.3s ease; }
.btn:hover { background: #4f46e5; }

Mistake 3 โ€” Using transition: all in production

โŒ Wrong โ€” all catches every property change, including ones you did not intend to animate:

.card { transition: all 0.3s ease; }
/* If JS adds a class that changes width, height, colour โ€” all animate unexpectedly */

โœ… Correct โ€” enumerate exactly the properties that should animate:

.card { transition: transform 0.25s ease, box-shadow 0.25s ease; }

▶ Try It Yourself

Quick Reference

Property Example Notes
transition transform 0.3s ease Shorthand: property duration timing delay
transition-property transform, opacity Prefer transform + opacity for performance
transition-duration 0.25s 100โ€“400ms for UI; longer for emphasis
transition-timing-function ease-out ease-out for arrivals; ease-in for departures
transition-delay 0.1s Stagger by incrementing delay on siblings
Multiple transitions transform 0.3s ease, opacity 0.2s ease Comma-separated โ€” each property independent
GPU properties transform, opacity Always prefer over width/height/top/left

🧠 Test Yourself

Where should the transition property be declared to ensure animation plays both on hover enter AND hover exit?





โ–ถ Try It Yourself