Lesson 11 of 12

CSS Transitions and Animations

Motion helps people understand what is happening on a page: a button that responds to hover feels clickable, a menu that slides in shows where it came from, and a spinner shows that something is loading. Two tools handle motion: CSS transitions for simple changes between two states, and animations for multi-step or repeating motion.

CSS transitions: animating a change

Normally, when a property changes (for example on :hover), the new value appears instantly. A transition makes the browser animate from the old value to the new one over time. You add the transition property to the element in its normal state, not the hover state, so it animates both ways.

PropertyExampleMeaning
transition-propertybackground-colorWhich property to animate
transition-duration200msHow long it takes
transition-timing-functionease-outThe speed curve (easing)
transition-delay0.1sWait before starting
transitiontransform 200ms ease-outShorthand for all of the above
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hover and focus transitions</title>
<style>
body { font-family: system-ui, sans-serif; padding: 30px; display: flex; gap: 24px; flex-wrap: wrap; }

.btn {
  background: #1971c2;
  color: white;
  border: 0;
  padding: 12px 20px;
  border-radius: 8px;
  font: inherit;
  cursor: pointer;
  transition: background-color 200ms ease-out, transform 200ms ease-out;
}
.btn:hover,
.btn:focus-visible {
  background: #1864ab;
  transform: translateY(-2px);
}

.card {
  width: 200px;
  padding: 20px;
  border-radius: 12px;
  background: white;
  box-shadow: 0 1px 3px rgb(0 0 0 / 20%);
  transition: transform 250ms ease, box-shadow 250ms ease;
}
.card:hover {
  transform: scale(1.04);
  box-shadow: 0 8px 24px rgb(0 0 0 / 20%);
}
</style>
</head>
<body>
<button class="btn">Reserve a table</button>
<div class="card">Hover me: Chettinad thali</div>
</body>
</html>

Avoid transition: all. It animates every property that changes, including ones you did not intend, and makes performance harder to reason about. List the properties you actually want to animate.

Easing and duration

Easing describes how speed changes during the motion. ease-out starts fast and slows down, which feels natural for things entering or responding to the user. ease-in is better for things leaving. linear suits spinners and progress bars. For precise control use cubic-bezier(), which design tools like Figma can export.

Keep interface motion short. Hover and focus effects feel snappy at around 100 to 250ms; larger panels may take 300 to 400ms. Anything longer makes the interface feel slow.

Keyframe animations

For motion that has several steps, repeats, or starts on its own without a state change, use an animation. First define the steps with @keyframes and a name, using from/to or percentages. Then attach it to an element with the animation property.

  • animation-name and animation-duration are the two you always need.
  • animation-iteration-count: a number or infinite.
  • animation-direction: alternate plays forwards then backwards.
  • animation-fill-mode: both keeps the first frame before it starts and the last frame after it ends.
  • The shorthand: animation: spin 1s linear infinite;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keyframes: fade-in, spinner and pulse</title>
<style>
body { font-family: system-ui, sans-serif; padding: 30px; display: grid; gap: 30px; justify-items: start; }

@keyframes fade-up {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes spin {
  to { transform: rotate(1turn); }
}

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

.message {
  animation: fade-up 600ms ease-out both;
  background: #ebfbee;
  padding: 16px;
  border-radius: 8px;
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #dee2e6;
  border-block-start-color: #e8590c;
  border-radius: 50%;
  animation: spin 900ms linear infinite;
}

.dot {
  width: 16px;
  height: 16px;
  background: #e03131;
  border-radius: 50%;
  animation: pulse 1.2s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .message, .dot { animation: none; }
  .spinner { animation-duration: 3s; }  /* slower, still shows activity */
}
</style>
</head>
<body>
<div class="message">Your order from Hotel Saravana has been placed.</div>
<div class="spinner" role="status" aria-label="Loading"></div>
<div class="dot" aria-hidden="true"></div>
</body>
</html>

Transition or animation?

Use a transition when…Use an animation when…
A property changes between two states, such as hover, focus or an added classThe motion has more than two steps
The change is triggered by the user or by JavaScriptIt should start on its own when the page loads
It should reverse smoothly when the state changes backIt should repeat or loop, like a spinner
Examples: button hover, menu open, colour changeExamples: loading indicator, attention pulse, entrance effect

In practice, most interface motion is built with CSS transitions. Keyframe animations are the spice: use them for a few deliberate moments rather than on every element.

Keep animations smooth

Browsers can animate transform and opacity very cheaply because they do not change the page layout. Animating properties like width, height, top or margin forces the browser to recalculate the layout on every frame, which can stutter on low-cost phones. To move something, use transform: translate(); to grow it, use scale(); to fade it, use opacity. The post CSS Animation Performance Tips goes further.

Respect reduced motion

Some people get dizzy, nauseous or distracted by motion, for example people with vestibular disorders. Operating systems let them switch on a “reduce motion” setting, and CSS can read it with the prefers-reduced-motion media query. When it is reduce, remove or tone down non-essential motion such as large movements, parallax and looping effects. Gentle fades are usually acceptable.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A motion-safe approach</title>
<style>
body { font-family: system-ui, sans-serif; padding: 30px; }

.banner {
  background: #fff0f6;
  padding: 20px;
  border-radius: 10px;
}

/* Only add movement when the user has not asked for less motion */
@media (prefers-reduced-motion: no-preference) {
  .banner {
    animation: slide-in 700ms ease-out both;
  }
}

@keyframes slide-in {
  from { transform: translateX(-40px); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
</style>
</head>
<body>
<div class="banner">Pongal offers start this Friday.</div>
<p>Turn on "reduce motion" in your operating system settings and reload: the banner appears without sliding.</p>
</body>
</html>

Tip: Wrapping motion in @media (prefers-reduced-motion: no-preference), as in the last example, is a safe default: motion is an extra that only users who are fine with it receive.

Watch out: Never put essential information only inside an animation, and avoid anything that flashes more than three times a second. Rapid flashing can trigger seizures and fails WCAG.

You can also trigger CSS transitions and animations from JavaScript by adding a class when the user clicks; see JS Events. In the final lesson, CSS Variables, you will store durations and easings as reusable tokens.

Further reading

Key takeaways

  • Transitions animate between two states; put transition on the normal state.
  • Keyframe animations use @keyframes plus the animation shorthand for multi-step or looping motion.
  • Use short durations (about 100–250ms for small UI changes) and easing such as ease-out.
  • Animate transform and opacity for smooth performance.
  • Honour prefers-reduced-motion and never flash content rapidly.

Quick quiz

Check what you learned – pick an answer.

1. Where should you put the transition property so a hover effect animates both in and out?

2. Which pair of properties is cheapest for the browser to animate?

3. What does @media (prefers-reduced-motion: reduce) detect?

Practice exercise

Create a "Add to cart" button that lifts slightly and changes colour on hover and keyboard focus over 150ms, and a small badge that pulses forever using @keyframes. Make sure the pulse is switched off when the user prefers reduced motion.

Show the solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Motion exercise</title>
<style>
body { font-family: system-ui, sans-serif; padding: 30px; }

.cart {
  position: relative;
  background: #2b8a3e;
  color: white;
  border: 0;
  padding: 12px 20px;
  border-radius: 8px;
  font: inherit;
  transition: transform 150ms ease-out, background-color 150ms ease-out;
}
.cart:hover,
.cart:focus-visible {
  transform: translateY(-3px);
  background: #237032;
}

.badge {
  position: absolute;
  top: -8px;
  right: -8px;
  background: #f03e3e;
  border-radius: 999px;
  padding: 2px 7px;
  font-size: 0.75rem;
  animation: pulse 1s ease-in-out infinite;
}

@keyframes pulse {
  50% { transform: scale(1.2); }
}

@media (prefers-reduced-motion: reduce) {
  .badge { animation: none; }
}
</style>
</head>
<body>
<button class="cart">Add to cart <span class="badge">3</span></button>
</body>
</html>

Progress is saved in this browser only.