Skip to main content

🎚️ CSS Transitions Fundamentals

Without transitions, style changes are a light switch β€” instant and jarring. With them, they become a dimmer β€” smooth and deliberate. This lesson teaches you to control the "in-between" so your interface feels responsive, guided, and alive.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Use the four transition longhands β€” property, duration, timing-function, delay β€” and the transition shorthand
  • Explain which CSS properties can and cannot be transitioned
  • Read and pick timing functions, including custom cubic-bezier() curves
  • Identify the events that trigger a transition (hover, focus, class changes, custom properties)
  • Write performant transitions and honor prefers-reduced-motion

Estimated Time: 30–40 minutes  β€’  Difficulty: Beginner–Intermediate

Hands-on: Build a button and a card whose hover and focus states glide smoothly.

In This Lesson

What Is a Transition?

A CSS transition tells the browser: "when this property changes, don't jump straight to the new value β€” glide there over a set amount of time." The declaration lives on the element's resting state; the change is triggered elsewhere (a :hover, a new class, a media query).

πŸ’‘ A useful mental model: Think of transitions as the tweening between two keyframes you never had to draw. You define the "before" and the "after"; the browser fills in every frame in between.

Good transitions do more than look nice. They guide the eye through a change, show cause and effect ("you clicked, here's the result"), and make relationships between elements clear. Overdone, they slow users down β€” so restraint is part of the skill.

graph TD A[transition] --> B[transition-property] A --> C[transition-duration] A --> D[transition-timing-function] A --> E[transition-delay] B --> B1[what changes] C --> C1[how long] D --> D1[the speed curve] E --> E1[wait before starting]

The Four Transition Properties

1. transition-property

Names which properties animate when they change. Use a specific property, a comma list, all, or none.

.a { transition-property: background-color; }
.b { transition-property: background-color, transform, opacity; }
.c { transition-property: all;  /* animate everything that changes */ }

⚠️ Not everything can transition

Only properties with a meaningful "halfway" value can animate. Colors, lengths, and transform interpolate fine. Discrete values do not: you cannot smoothly transition display, font-family, float, or background-image. (Modern browsers add transition-behavior: allow-discrete for special cases like display, but plan around the classic limitation.)

Transitionable βœ…Not transitionable ❌
color, background-color, border-colordisplay
width, height, margin, paddingfont-family
opacity, box-shadow, filterfloat, text-align
transform, top/right/bottom/leftbackground-image

2. transition-duration

How long the change takes, in seconds (s) or milliseconds (ms). It defaults to 0s β€” which is why forgetting it means "no visible transition."

.a { transition-duration: 0.3s; }
.b { transition-duration: 300ms; } /* identical */

/* One duration per property, matched in order */
.c {
  transition-property: background-color, transform, opacity;
  transition-duration: 0.2s, 0.5s, 1s;
}

πŸ’‘ Duration guidelines

  • 100–200ms: instant feedback β€” button hovers, focus rings.
  • 200–500ms: the sweet spot for most UI state changes.
  • 500ms–1s: larger, attention-drawing changes; use sparingly.
  • Over 1s: reserved for deliberate, dramatic moments.

3. transition-timing-function

Controls the speed curve β€” how the intermediate values are distributed over the duration. Covered in depth in Section 4.

4. transition-delay

How long to wait before the transition begins. Great for staggering elements or preventing hover-jitter.

.tooltip { transition: opacity 0.2s ease 0.4s; } /* 0.4s delay before fade-in */

The Shorthand

In practice you almost always use the transition shorthand, which packs all four values in one line. The order is property, duration, timing-function, delay. The first time value is always duration; the second (if present) is delay.

/* property  duration  timing  delay */
.btn { transition: background-color 0.3s ease 0s; }

/* Duration alone is enough β€” timing defaults to ease, delay to 0 */
.card { transition: box-shadow 0.3s; }

/* Multiple transitions, comma-separated */
.hero {
  transition:
    transform 0.4s ease-out,
    opacity   0.6s linear 0.1s;
}

βœ… Best practice: be specific

Prefer listing exact properties over transition: all. all is convenient but animates everything that happens to change β€” including layout properties you didn't intend β€” which causes surprise jank. Naming transform and opacity keeps transitions cheap and predictable.

Timing Functions & Bezier Curves

A timing function maps elapsed time to progress. The same 0.5s can feel snappy, mechanical, or bouncy depending on the curve.

KeywordFeel
linearConstant speed β€” mechanical, good for spinners
ease (default)Slow start, quick middle, slow end β€” natural
ease-inSlow start, fast finish β€” good for exits
ease-outFast start, slow finish β€” good for entrances
ease-in-outSlow at both ends β€” smooth for round trips

Custom cubic-bezier() curves

Every keyword is shorthand for a cubic-bezier(x1, y1, x2, y2). The curve starts at (0,0) and ends at (1,1); the two control points bend it. Pushing a y-value above 1 or below 0 creates overshoot β€” the basis of "bounce" effects.

Three cubic-bezier timing curves A graph with time on the x-axis and progress on the y-axis showing ease-out, ease-in, and an overshooting bounce curve. time β†’ progress β†’ ease-out ease-in bounce (overshoot)
Figure 1 β€” Timing curves shape how progress accumulates over time. The dashed bounce curve dips below 0 and rises above 1 to overshoot the target.
/* A playful overshoot / bounce */
.pop { transition-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55); }

Tools like cubic-bezier.com let you drag the control points and preview the motion. There is also steps(n), which jumps between n discrete states instead of interpolating β€” handy for sprite-sheet or "ticking clock" animations.

What Triggers a Transition

A transition fires whenever a watched property's value changes. That change can come from several sources:

Pseudo-classes (the most common)

.button {
  background-color: #2563eb;
  transition: background-color 0.3s ease, transform 0.1s ease;
}
.button:hover  { background-color: #1e40af; }
.button:active { transform: scale(0.96); }
.button:focus-visible { box-shadow: 0 0 0 3px rgba(37,99,235,0.5); }

Class changes from JavaScript

const box = document.querySelector('.box');
box.addEventListener('click', () => box.classList.toggle('expanded'));
.box { transform: scale(1); transition: transform 0.4s ease-out; }
.box.expanded { transform: scale(1.5); }

Media queries and custom properties

A layout that changes at a breakpoint, or a CSS variable updated at runtime (a theme switch), both trigger transitions on the affected properties.

:root { --brand: #2563eb; }
.chip { background: var(--brand); transition: background 0.6s ease; }
/* Updating --brand in JS animates every .chip */

πŸ“– How the process plays out

On trigger the browser waits out the delay, then interpolates from the current value to the target across the duration, distributed by the timing function. If the state reverts mid-flight (you move the mouse away), the transition reverses from where it is β€” it doesn't snap. New triggers can interrupt an in-progress transition.

flowchart LR A[Resting state] --> B[Trigger: hover / class / var] B --> C[Wait out delay] C --> D[Interpolate over duration] D --> E[Target state reached] E -->|state reverts| A

Practical Patterns

The lift-and-glow button

.btn-elevate {
  box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.btn-elevate:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(0,0,0,0.3);
}

The growing underline nav link

.nav-link { position: relative; text-decoration: none; }
.nav-link::after {
  content: '';
  position: absolute; left: 0; bottom: -4px;
  width: 100%; height: 2px;
  background: #2563eb;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.3s ease;
}
.nav-link:hover::after,
.nav-link.active::after { transform: scaleX(1); }

Transitioning "to display: none"

Because display isn't animatable in the classic model, fade panels with opacity plus visibility, delaying visibility so it flips only after the fade:

.panel {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0s 0.3s;
}
.panel.open {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.3s ease; /* visibility flips immediately on the way in */
}

Performance & Accessibility

⚠️ Animate the cheap properties

Changing width, height, top/left, or margin forces the browser to recompute layout (reflow) every frame β€” expensive, especially on mobile. transform and opacity are the exceptions: they run on the GPU and skip layout entirely.

Cheap (GPU) βœ…Expensive (reflow) ❌
transformwidth / height
opacitytop / right / bottom / left
filtermargin / padding
/* Prefer scaling with transform over animating width/height */
.grow { transition: transform 0.3s; }
.grow:hover { transform: scale(1.5); }

The will-change hint can pre-promote an element to its own layer, but overuse wastes memory β€” apply it only to elements that truly animate, and remove it when idle.

πŸ’‘ Always respect reduced motion

Some users get motion sickness or vestibular symptoms from animation. Honor their OS setting:

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

Also: never remove focus outlines without replacing them, and keep color transitions above a 4.5:1 contrast ratio throughout.

Hands-on Exercise

πŸ‹οΈ Make a Button and Card Come Alive

Objective: Combine several transitioned properties, choose sensible durations and curves, and keep it accessible.

Instructions:

  1. Style a button that, on hover, changes background color (0.3s) and lifts with transform: translateY(-2px) plus a bigger shadow.
  2. Add a visible :focus-visible ring that transitions in β€” don't remove the outline without replacing it.
  3. Make a card whose shadow and translateY transition on hover, and whose inner "image" area scales to 1.1 (use overflow: hidden so it zooms within the frame).
  4. Give the card's zoom an ease-out curve at ~0.5s for a gentle finish.
  5. Wrap everything in a prefers-reduced-motion guard.
πŸ’‘ Hint

Put the transition on the resting selector, not on :hover. List the exact properties (transform, box-shadow, background-color) rather than all so nothing unexpected animates.

βœ… Sample solution
.btn {
  background: #2563eb; color: #fff; padding: 0.7rem 1.4rem;
  border: none; border-radius: 6px; box-shadow: 0 2px 5px rgba(0,0,0,.2);
  transition: background-color 0.3s ease, transform 0.2s ease,
              box-shadow 0.2s ease;
}
.btn:hover { background: #1e40af; transform: translateY(-2px);
             box-shadow: 0 6px 16px rgba(0,0,0,.3); }
.btn:focus-visible { outline: none;
             box-shadow: 0 0 0 3px rgba(37,99,235,.5); }

.card { border-radius: 10px; overflow: hidden;
        box-shadow: 0 3px 10px rgba(0,0,0,.1);
        transition: transform 0.3s ease, box-shadow 0.3s ease; }
.card:hover { transform: translateY(-6px);
              box-shadow: 0 16px 30px rgba(0,0,0,.2); }
.card__img { transition: transform 0.5s ease-out; }
.card:hover .card__img { transform: scale(1.1); }

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 0.01ms !important; }
}

🎯 Quick Quiz

Question 1: In transition: opacity 0.2s ease 0.4s;, what does 0.4s mean?

Question 2: Which property change will not animate with a transition?

Question 3: For a smooth 60fps hover-grow, which is the best property to animate?

Summary & Quiz

πŸŽ‰ Key Takeaways

  • A transition needs at least a property and a non-zero duration; it lives on the resting state.
  • The shorthand order is property duration timing delay; prefer naming properties over all.
  • Timing functions shape the motion; cubic-bezier() can even overshoot for bounce.
  • Transitions fire on any value change β€” hover, focus, class swaps, media queries, custom properties.
  • Animate transform and opacity for speed, and always honor prefers-reduced-motion.

πŸ“š Further Reading

πŸš€ What's Next?

Transitions animate between two states, but the states themselves come from the transform property β€” translate, scale, rotate, and their 3D cousins. Next we dive into 2D and 3D transforms so you have rich end-states worth transitioning to.

πŸŽ‰ Smooth work!

Your interfaces can now move with intention. Let's give them somewhere to move to.