CSS Transitions

Difficulty: Intermediate

CSS transitions provide a way to animate changes in CSS property values smoothly over a specified duration instead of having them take effect instantly. When a property value changes - triggered by a pseudo-class like :hover, :focus, or :active, or by adding/removing a class via JavaScript - the transition interpolates between the old and new values, creating a smooth visual effect. Transitions are the simplest form of CSS animation and are ideal for state changes like hover effects, focus states, and toggle interactions.

The transition-property specifies which CSS properties should be animated. You can target a specific property (like opacity, transform, or background-color), list multiple properties separated by commas, or use the keyword all to transition every animatable property. Using all is convenient but can cause unexpected animations and performance issues - it is better practice to explicitly list only the properties you intend to animate. Not all CSS properties are animatable; properties like display, font-family, and position cannot be transitioned because there are no intermediate values between their states.

The transition-duration sets how long the animation takes to complete, specified in seconds (s) or milliseconds (ms). A duration of 0.2s to 0.3s feels snappy and responsive for hover effects, while 0.4s to 0.6s works well for more significant changes like expanding panels or sliding drawers. Durations below 0.1s feel instant (defeating the purpose of the transition), and durations above 1s feel sluggish and delay user interaction.

The transition-timing-function controls the acceleration curve of the transition - how the speed varies over the duration. Built-in keywords include ease (starts slow, speeds up, slows down - the default), linear (constant speed), ease-in (starts slow), ease-out (ends slow), and ease-in-out (starts and ends slow). For more control, the cubic-bezier() function lets you define custom curves. The steps() function creates a stepped animation with discrete jumps rather than smooth interpolation - useful for sprite sheet animations.

The transition-delay adds a wait time before the transition begins, also specified in seconds or milliseconds. Delays are useful for creating staggered animations on multiple elements or preventing accidental triggers on hover. A small delay (0.05s-0.1s) on dropdown menus prevents them from opening when a user's cursor briefly passes over the trigger. The shorthand transition property combines all four values: property duration timing-function delay. Multiple transitions can be specified on a single element by comma-separating them, allowing different durations and timing functions for different properties.

Code examples

Basic Transition Properties

/* Individual transition properties */
.button {
  background-color: #3b82f6;
  color: white;
  padding: 12px 24px;
  border: none;
  border-radius: 8px;
  cursor: pointer;

  transition-property: background-color, transform;
  transition-duration: 0.3s;
  transition-timing-function: ease;
  transition-delay: 0s;
}

.button:hover {
  background-color: #2563eb;
  transform: translateY(-2px);
}

.button:active {
  transform: translateY(0);
  transition-duration: 0.1s; /* Faster on click */
}

/* Shorthand equivalent */
.button-shorthand {
  transition: background-color 0.3s ease, transform 0.3s ease;
}

The transition is defined on the base state, not the :hover state. When the user hovers, the browser smoothly interpolates background-color and transform over 0.3 seconds. On :active, a shorter duration makes the press feel more responsive.

Timing Functions Compared

.box {
  width: 80px;
  height: 80px;
  background: #3b82f6;
  transition: transform 1s;
}

/* Different timing functions */
.linear       { transition-timing-function: linear; }
.ease         { transition-timing-function: ease; }
.ease-in      { transition-timing-function: ease-in; }
.ease-out     { transition-timing-function: ease-out; }
.ease-in-out  { transition-timing-function: ease-in-out; }

/* Custom cubic-bezier curves */
.bounce {
  transition-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.snappy {
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* Steps for discrete jumps */
.step-animation {
  transition-timing-function: steps(4, end);
}

/* On hover, all boxes move right */
.container:hover .box {
  transform: translateX(300px);
}

linear moves at constant speed. ease (default) starts slow, accelerates, then decelerates. ease-in starts slow and accelerates. ease-out starts fast and decelerates. cubic-bezier() allows custom curves - values outside 0-1 on the y-axis create overshoot/bounce effects.

Multiple Transitions with Different Timings

.card {
  background: white;
  padding: 24px;
  border-radius: 12px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  opacity: 1;

  /* Different duration and timing for each property */
  transition:
    transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
    box-shadow 0.3s ease,
    opacity 0.2s ease;
}

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

.card.hidden {
  opacity: 0;
  transform: translateY(20px);
}

/* Staggered delays on child elements */
.card:nth-child(1) { transition-delay: 0s; }
.card:nth-child(2) { transition-delay: 0.1s; }
.card:nth-child(3) { transition-delay: 0.2s; }
.card:nth-child(4) { transition-delay: 0.3s; }

Comma-separated transitions let you animate different properties with different durations and timing functions. Staggered delays on child elements create a cascading animation effect as cards appear one after another.

Transition Gotchas and Best Practices

/* BAD: 'all' can cause unexpected transitions */
.bad-example {
  transition: all 0.3s ease;
  /* This will also animate width, height, margin, padding
     if any of those change, potentially causing layout thrashing */
}

/* GOOD: Explicitly list properties */
.good-example {
  transition: transform 0.3s ease, opacity 0.3s ease;
}

/* Cannot transition display property */
.no-transition {
  display: none;
  transition: opacity 0.3s; /* This will NOT work */
}

/* Workaround: Use opacity + visibility instead of display */
.fade-element {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}
.fade-element.hidden {
  opacity: 0;
  visibility: hidden;
}

/* Prefer transform/opacity for smooth 60fps transitions */
.smooth {
  /* Good: GPU-accelerated */
  transition: transform 0.3s ease, opacity 0.3s ease;
}
.janky {
  /* Bad: Triggers layout recalculation */
  transition: width 0.3s ease, height 0.3s ease, top 0.3s ease;
}

Avoid transition: all - be explicit. Display cannot be transitioned; use opacity + visibility instead. For smooth 60fps performance, prefer transitioning transform and opacity (composited properties) over layout-triggering properties like width, height, top, and left.

Key points

Concepts covered

transition-property, transition-duration, transition-timing-function, transition-delay, Shorthand