Skip to content
AgileLogics
6 min read

Transitions

Prerequisites: Transforms

A transition animates a property smoothly when its value changes — on hover, focus, or when JavaScript toggles a class.

.button {
  background: #0b57d0;
  transition: background-color 150ms ease-out, transform 150ms ease-out;
}

.button:hover {
  background: #083b8a;
  transform: translateY(-1px);
}

The shorthand is transition: property duration timing-function delay. List several transitions separated by commas.

  • Property — which property to animate. all is convenient but can animate things you didn't intend; name them.
  • Duration — 100–300ms feels responsive for interface feedback.
  • Timing function — ease (default), linear, ease-in, ease-out (good for things entering), ease-in-out, cubic-bezier(...), steps(n), or linear(...) for custom curves such as springs.
  • Delay — wait before starting.

What can transition

Anything with values in between: colors, lengths, opacity, transforms, shadows. Discrete properties like display can't interpolate — they flip at once. Modern browsers let you transition them with transition-behavior: allow-discrete, used with @starting-style for entry animations:

.toast {
  transition: opacity 200ms, translate 200ms, display 200ms allow-discrete;
}

.toast[hidden] {
  display: none;
  opacity: 0;
  translate: 0 1rem;
}

@starting-style {
  .toast:not([hidden]) {
    opacity: 0;
    translate: 0 1rem;
  }
}

@starting-style defines where the transition starts when an element first appears (Chrome and Edge 117, Safari 17.5, Firefox 129).

Watching a transition from JavaScript

<style>
  .box { width: 100px; transition: width 300ms linear; }
  .box.wide { width: 300px; }
</style>
<div class="box"></div>
<script>
  const box = document.querySelector(".box");
  box.addEventListener("transitionend", e => {
    console.log("transitionend:", e.propertyName, e.elapsedTime + "s", "width now", getComputedStyle(box).width);
  });
  getComputedStyle(box).width;
  box.classList.add("wide");
  console.log("running transitions:", box.getAnimations().length);
</script>
running transitions: 1
transitionend: width 0.3s width now 300px

Reading getComputedStyle(box).width before adding the class makes sure the browser has a starting value; otherwise a change made in the same frame the element is created may not animate.

Respect reduced motion

@media (prefers-reduced-motion: reduce) {
  .button { transition-duration: 0.01ms; }
}

Color and opacity fades are generally fine; large movement, zooming, and parallax are what cause problems. Many teams do the reverse — only add motion inside @media (prefers-reduced-motion: no-preference).

Practice

Make .card lift 4px and gain a larger shadow on hover and on :focus-within, over 200ms, only when the user hasn't asked for reduced motion.

Show solution
.card {
  box-shadow: 0 1px 3px rgb(0 0 0 / 12%);
}

.card:hover,
.card:focus-within {
  box-shadow: 0 8px 20px rgb(0 0 0 / 16%);
}

@media (prefers-reduced-motion: no-preference) {
  .card {
    transition: transform 200ms ease-out, box-shadow 200ms ease-out;
  }

  .card:hover,
  .card:focus-within {
    transform: translateY(-4px);
  }
}