Skip to content

CSS Transitions — Animating State Changes

Without transitions, every CSS state change is instantaneous — a button changes color the moment your cursor touches it, a card jumps to its hovered position with no movement. With transitions, that change animates smoothly over time. The difference between a site that feels polished and one that feels abrupt is often a single CSS property.

A CSS transition watches a property for changes and animates the shift from one value to another. The animation is triggered by a state change — most commonly :hover, but also :focus, :active, or a class being toggled by JavaScript.

.btn {
background-color: #2c4a1e;
transition: background-color 200ms ease;
}
.btn:hover {
background-color: #3a5c26;
}

When the cursor enters the button, the background color animates from #2c4a1e to #3a5c26 over 200 milliseconds. When the cursor leaves, it animates back.

Which CSS property to animate. Use specific property names rather than all:

transition-property: background-color; /* only animate this */
transition-property: all; /* animate everything — avoid this */

all is tempting but problematic — it animates every property that changes on that element, including properties you did not intend to animate, and can cause unexpected jank. Target specific properties instead.

How long the animation takes. Use milliseconds (ms) or seconds (s):

transition-duration: 150ms; /* fast — micro-interactions, buttons */
transition-duration: 250ms; /* medium — card hover effects */
transition-duration: 400ms; /* slow — use sparingly */

For UI interactions, stay between 100ms and 400ms. Below 100ms is imperceptible; above 400ms feels sluggish.

The acceleration curve — how the animation speeds up and slows down across its duration:

  • ease (default) — starts slow, speeds up through the middle, ends slow. Feels natural for most interactions.
  • linear — constant speed throughout. Feels mechanical. Good for loading spinners.
  • ease-in — starts slow, ends fast. Good for elements leaving the screen.
  • ease-out — starts fast, ends slow. Good for elements entering the screen.
  • ease-in-out — slow at both ends. Smooth and deliberate.

For hover effects, ease is almost always the right choice.

How long to wait before the animation begins:

transition-delay: 100ms; /* wait 100ms, then start the animation */

Useful for staggered sequences but rarely needed for basic hover interactions.

Write all four values in a single declaration: property duration timing-function delay:

.tour-card {
transition: transform 250ms ease;
}

For multiple properties, comma-separate them:

.tour-card {
transition: transform 250ms ease, box-shadow 250ms ease;
}

The critical rule: transition on the default state

Section titled “The critical rule: transition on the default state”

This is the single most common transition mistake. If you put transition on the :hover rule instead of the default rule, the animation only plays when entering the hover state — it snaps back instantly on mouse-out:

/* WRONG — only animates in, snaps out */
.btn:hover {
background-color: #3a5c26;
transition: background-color 200ms ease;
}
/* CORRECT — animates in both directions */
.btn {
background-color: #2c4a1e;
transition: background-color 200ms ease; /* on the default state */
}
.btn:hover {
background-color: #3a5c26;
}

The transition on the default state applies every time that property changes — entering hover and leaving it. Always define transitions here.

transform is the preferred property for animating position and size. The browser renders transform animations using the GPU — it does not recalculate layout on every frame. This keeps animations smooth even on complex pages.

Two transforms used throughout the STO site:

transform: translateY(-6px); /* moves the element 6px upward */
transform: scale(1.04); /* scales the element to 104% */

Contrast this with animating layout properties:

/* Avoid — causes layout recalculation on every frame */
.tour-card:hover {
top: -6px; /* requires position: relative and triggers layout */
}
/* Preferred — GPU composited, no layout cost */
.tour-card:hover {
transform: translateY(-6px);
}

Tour card hover lift:

.tour-card {
transition: transform 250ms ease, box-shadow 250ms ease;
}
.tour-card:hover {
transform: translateY(-6px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
}

Button hover:

.btn {
transition: transform 150ms ease, box-shadow 150ms ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}

Buttons use a shorter duration than cards — 150ms versus 250ms — because buttons are small and a faster response feels more immediate.

Nav link underline reveal:

A classic pattern: reveal an underline on hover using a ::after pseudo-element that grows from width: 0 to width: 100%:

.nav-link {
position: relative;
text-decoration: none;
color: #f5f0eb;
}
.nav-link::after {
content: "";
position: absolute;
bottom: -4px;
left: 0;
width: 0;
height: 2px;
background-color: #a8c97f;
transition: width 200ms ease; /* transition on the default state */
}
.nav-link:hover::after {
width: 100%;
}

Note that the transition is on the default .nav-link::after rule, not on .nav-link:hover::after — the same rule that applies to mouse-out applies to mouse-in.

Some users experience discomfort or motion sickness from animation. In Module 07 (Responsive Design) you will learn the prefers-reduced-motion media query, which lets users opt out of animation at the OS level. Keep transitions brief and purposeful — they should enhance usability, not distract from it.

Add transitions to the STO site:

  1. Add a card hover transition to style.css:
.tour-card {
transition: transform 250ms ease, box-shadow 250ms ease;
}
.tour-card:hover {
transform: translateY(-6px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
}
  1. Add a button transition:
.btn {
transition: transform 150ms ease, box-shadow 150ms ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
  1. Open the browser. Hover over a tour card — it should lift smoothly. Move the cursor off quickly — it should animate back, not snap. This confirms the transition is defined on the default state.

  2. Open DevTools. While hovering a card, watch the Computed tab — the transform value updates in real time. In Chrome or Edge, open More Tools → Animations to see the transition timeline.

  3. Change the card transition from 250ms ease to 1000ms linear. Hover the card — it should feel slow and mechanical. Revert to 250ms ease and notice the difference.

  • transition animates the change between two CSS property values, triggered by state changes like :hover and :focus.
  • Sub-properties: transition-property (what), transition-duration (how long), transition-timing-function (pace curve), transition-delay (wait before start).
  • Always define transition on the default state, not on the :hover rule — this ensures animation plays in both directions.
  • Use comma-separated transitions for multiple properties: transition: transform 250ms ease, box-shadow 250ms ease.
  • transform (translateY, scale) is preferred over top/left/width/height for animation — it is GPU-accelerated and does not trigger layout recalculation.
  • Timing functions: ease feels natural, linear feels mechanical, ease-out enters smoothly, ease-in exits smoothly.

Module 05 introduces Flexbox — the layout system that controls how elements are arranged in a row or column on the page.