Part 1 of 4 · CSS Shine Lab

The transition property

Animate an element's property changes smoothly over time instead of snapping between states. The easiest way to start adding shine.

  • Innovation

Reach for transition when an element changes between two states and you want the change to ease rather than snap: a button on hover, an input on focus, a panel opening. Easing it over a couple of hundred milliseconds is often all it takes to make an interface feel responsive, draw attention to what the user just did, and add a little life to the work.

It is as safe a bet as CSS gets. Transitions have worked in every major browser for well over a decade.

Implementation

Declare transition on the element’s resting state, not on :hover, list the properties to ease, and keep the durations short. Between 150ms and 300ms feels responsive, and much longer starts to feel sluggish. Favour properties that are cheap to animate, like transform and opacity, and always pair it with a prefers-reduced-motion guard so the change stays instant for anyone who asks for less motion.

A button that lifts

Several properties eased at once. On hover or focus the button rises, gains a shadow, and inverts its colours, all from one transition declaration.

View source

HTML

<button type="button" class="transition-button">
  Hover or focus me
</button>

CSS

.transition-button {
  display: inline-flex;
  align-items: center;
  padding: 0.9rem 1.6rem;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface-elevated);
  color: var(--color-text);
  font: inherit;
  cursor: pointer;

  /* The transition property. Each listed property is interpolated over 0.3s
     with an ease curve instead of snapping instantly. Shorthand order per item:
     <property> <duration> <timing-function> <delay>. */
  transition:
    transform 0.3s ease,
    box-shadow 0.3s ease,
    background-color 0.3s ease,
    color 0.3s ease;
}

.transition-button:hover,
.transition-button:focus-visible {
  transform: translateY(-6px) scale(1.02);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.35);
  background-color: var(--color-accent);
  color: var(--color-on-accent);
}

/* Honour a reduced-motion preference: keep the state change, drop the animation. */
@media (prefers-reduced-motion: reduce) {
  .transition-button {
    transition: none;
  }
}

You can transition pseudo-elements too. The link’s colour eases while an ::after underline grows from zero width to full.

View source

HTML

<a href="#" class="transition-link">
  Read the docs
</a>

CSS

.transition-link {
  position: relative;
  display: inline-block;
  color: var(--color-text);
  font-weight: 600;
  text-decoration: none;

  /* Ease the text colour as the underline (below) grows in. */
  transition: color 0.25s ease;
}

/* The underline is a pseudo-element whose width transitions from 0 to full. */
.transition-link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -2px;
  width: 0;
  height: 2px;
  background: var(--color-accent);
  transition: width 0.25s ease;
}

.transition-link:hover,
.transition-link:focus-visible {
  color: var(--color-accent);
}

.transition-link:hover::after,
.transition-link:focus-visible::after {
  width: 100%;
}

@media (prefers-reduced-motion: reduce) {
  .transition-link,
  .transition-link::after {
    transition: none;
  }
}

A search field that grows

Form controls animate like anything else. On focus this input widens while its border and focus ring ease in together.

View source

HTML

<input
  type="search"
  class="transition-search"
  placeholder="Search…"
  aria-label="Search"
/>

CSS

.transition-search {
  width: 12rem;
  max-width: 100%;
  padding: 0.6rem 0.9rem;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-full);
  background: var(--color-surface-elevated);
  color: var(--color-text);
  font: inherit;

  /* A form control is just another element: width, border and shadow all ease. */
  transition:
    width 0.3s ease,
    border-color 0.3s ease,
    box-shadow 0.3s ease;
}

.transition-search::placeholder {
  color: var(--color-text-tertiary);
}

.transition-search:focus {
  width: 20rem;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-subtle);
}

@media (prefers-reduced-motion: reduce) {
  .transition-search {
    transition: none;
  }
}

An image thumbnail

Two properties on two elements, running together: the image scales up while its caption fades in over the top.

Aurora — hover to reveal
View source

HTML

<figure class="transition-thumb">
  <div class="transition-thumb__image"></div>
  <figcaption class="transition-thumb__caption">
    Aurora — hover to reveal
  </figcaption>
</figure>

CSS

.transition-thumb {
  position: relative;
  overflow: hidden;
  width: 18rem;
  max-width: 100%;
  aspect-ratio: 3 / 2;
  margin: 0;
  border-radius: var(--radius-md);
}

/* A stand-in for a photo — the "image" itself scales up on hover. */
.transition-thumb__image {
  width: 100%;
  height: 100%;
  background: linear-gradient(135deg, var(--isotoma-teal), var(--isotoma-navy));
  transition: transform 0.4s ease;
}

/* The caption fades in over the top — a separate property (opacity) on a
   separate element, running alongside the image's transform. */
.transition-thumb__caption {
  position: absolute;
  inset: auto 0 0 0;
  padding: 1.5rem 1rem 0.75rem;
  color: #fff;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.65));
  opacity: 0;
  transition: opacity 0.4s ease;
}

.transition-thumb:hover .transition-thumb__image,
.transition-thumb:focus-within .transition-thumb__image {
  transform: scale(1.08);
}

.transition-thumb:hover .transition-thumb__caption,
.transition-thumb:focus-within .transition-thumb__caption {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .transition-thumb__image,
  .transition-thumb__caption {
    transition: none;
  }
}

Back to CSS Shine Lab