Choreographing Motion With transition-delay

When several things change at once — a card lifts, its image zooms, its caption slides up, its button fades in — running every transition simultaneously produces a single undifferentiated lurch. Staggering them a few tens of milliseconds apart turns the same changes into a sequence the eye can follow: the card moves, then its content responds. transition-delay is the tool, and used with care it adds clarity rather than slowness. This page covers delays per property and per element, delays that differ by direction, staggered lists, and the budget that keeps choreography from becoming waiting. It belongs to CSS Transition Fundamentals in the CSS-Only Micro-Interactions & Animations guide.

Why sequence at all

Motion communicates cause and effect. When a user hovers a card and everything in it changes at the same instant, the motion says only "something happened". When the card lifts first and its caption follows a beat later, the motion says "the card responded to you, and revealed more". That ordering makes interfaces feel considered and helps users understand which element is primary. A little planning up front — which element leads, which follow, and how long the whole sequence may take — saves hours of tweaking individual delays later.

The same principle, applied carelessly, produces sluggish interfaces. Each delay adds to the time before the final state is visible. A hover whose last element settles 600 milliseconds after the pointer arrives feels unresponsive, however elegant the sequence. Choreography therefore always comes with a budget.

A staged hover, under 400 milliseconds Four bars on a time axis. The card lift starts at zero and lasts 250 milliseconds. The image zoom starts at 40. The caption starts at 80. The button starts at 120 and ends near 370 milliseconds, inside a 400 millisecond budget. Cause first, effects a beat later 0 100 200 300 400ms card lift image zoom caption button budget 40ms steps are enough to read as a sequence without feeling slow.

The complete implementation

The card below sequences four changes on hover and focus, reverses them in the opposite order on exit, and removes the choreography for reduced motion.

Live demoCard hover choreographed with transition-delay
Hover or Tab to the card: it lifts first, then the image zooms, the caption rises and the link appears, one beat apart; leaving reverses the order.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Transition choreography</title>
<style>
  :root {
    --step: 40ms;                                   /* one beat */
    --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  }

  body { font: 15px/1.5 system-ui, sans-serif; margin: 2rem; }

  .story {
    display: grid;
    max-width: 18rem;
    border-radius: 14px;
    overflow: hidden;
    background: #0f172a;
    color: #f8fafc;
    text-decoration: none;
    /* EXIT timing lives on the base rule: the card settles LAST, so its
       delay is the largest; the button leaves first. */
    transition: translate 220ms var(--ease-out) calc(var(--step) * 2);
  }

  .story__img {
    aspect-ratio: 4 / 3;
    background: linear-gradient(135deg, #38bdf8, #6366f1);
    transition: scale 260ms var(--ease-out) var(--step);
  }
  .story__caption {
    padding: 0.9rem 1rem 0.2rem;
    translate: 0 0.5rem;
    opacity: 0.7;
    transition: translate 220ms var(--ease-out) 0ms, opacity 220ms ease 0ms;
  }
  .story__cta {
    margin: 0 1rem 1rem;
    opacity: 0;
    transition: opacity 160ms ease 0ms;
  }

  /* ENTRY timing lives on the hovered state: cause first (the card),
     then image, caption and button, one beat apart. */
  .story:is(:hover, :focus-visible) {
    translate: 0 -4px;
    transition: translate 220ms var(--ease-out) 0ms;
  }
  .story:is(:hover, :focus-visible) .story__img {
    scale: 1.05;
    transition: scale 260ms var(--ease-out) var(--step);
  }
  .story:is(:hover, :focus-visible) .story__caption {
    translate: 0 0;
    opacity: 1;
    transition: translate 220ms var(--ease-out) calc(var(--step) * 2),
                opacity 220ms ease calc(var(--step) * 2);
  }
  .story:is(:hover, :focus-visible) .story__cta {
    opacity: 1;
    transition: opacity 160ms ease calc(var(--step) * 3);
  }
  .story:focus-visible { outline: 3px solid #facc15; outline-offset: 3px; }

  /* Reduced motion: no movement, no waiting. The CTA simply appears. */
  @media (prefers-reduced-motion: reduce) {
    .story, .story * { transition-duration: 1ms !important; transition-delay: 0ms !important; }
    .story:is(:hover, :focus-visible) { translate: none; }
    .story:is(:hover, :focus-visible) .story__img { scale: none; }
  }
</style>
</head>
<body>
  <a class="story" href="/stories/coast/">
    <div class="story__img" role="img" aria-label="Coastline"></div>
    <div class="story__caption"><h3>Along the coast</h3></div>
    <span class="story__cta">Read the story →</span>
  </a>
</body>
</html>

The reduced-motion rule uses !important deliberately: it must beat every per-state transition declaration, including the hover ones with higher specificity, and a motion-preference override is one of the few places where a blanket !important is the right tool. The button is still revealed — its opacity changes — so the information is not lost, only the sequencing.

The key technique: entry and exit are separate sequences

Because the state being entered supplies the transition values, the entry sequence is written on the hover state and the exit sequence on the base state. That lets the two sequences run in opposite orders, which is what makes choreography feel natural: things leave in the reverse of how they arrived.

Arrive in one order, leave in the reverse Entry, declared on the hover state: card 0, image 40, caption 80, button 120 milliseconds. Exit, declared on the base state: button 0, caption 0, image 40, card 80 milliseconds. Delays per direction entry (on :hover) card 0ms image 40ms caption 80ms button 120ms exit (on base rule) button 0ms caption 0ms image 40ms card 80ms The exit is shorter overall: the details leave together, the card settles last.

Exits are usually faster and less staged than entries. The user has moved on, and a long exit sequence keeps stale content on screen. Collapsing the details' exit delays to zero, as the example does, and letting only the card settle last gives a quick, tidy departure. The reasoning behind asymmetric timing is developed in Entry vs Exit Easing.

Negative delays and delays inside one element

Delays apply per property, so a single element can sequence its own changes: a button's background can change immediately while its icon slides 60 milliseconds later, both declared in one transition list. That is often enough choreography for small components, with no need to coordinate several elements.

Negative delays are the less familiar tool. transition-delay: -100ms starts a transition as if it had begun 100 milliseconds ago — it jumps to the point it would have reached and continues from there. That sounds odd for interactive states, but it is useful in two situations.

Syncing late joiners. If several elements should move together but one of them only begins transitioning after a layout change, a negative delay equal to the lag lets it catch up to the others' phase rather than trailing them.

Trimming slow starts. A long ease-in curve spends its first stretch barely moving. A small negative delay skips the imperceptible beginning, making the motion start visibly sooner without changing the curve's overall shape.

Both are refinements; most choreography needs only small positive delays. But knowing that delays can be negative helps when a sequence looks subtly out of step and positive adjustments only make the whole thing slower.

Staggering many items

Delays also stagger lists: each item waits a little longer than the previous. Custom properties make that systematic without writing a rule per item.

.results > li {
  transition: opacity 200ms ease, translate 200ms cubic-bezier(0.22, 1, 0.36, 1);
  /* --i is set per item: style="--i: 0", "--i: 1" … or via :nth-child. */
  transition-delay: calc(min(var(--i, 0), 6) * 30ms);
}

.results.is-loading > li {
  opacity: 0;
  translate: 0 0.5rem;
}

The min(var(--i), 6) cap is the budget in code: the first seven items stagger, and everything after arrives together with the seventh, so a list of fifty results is fully visible within about 400 milliseconds rather than two seconds. Staggered List Animations With Custom Properties covers the keyframe version and the indexing techniques in more detail.

The budget

A few rules keep choreography helpful:

  • Keep steps small. 30 to 60 milliseconds per step is enough for the eye to perceive a deliberate order; larger steps start to feel like separate events.
  • Keep the total short. The last element should settle within about 400 milliseconds of the trigger for routine interactions.
  • Stagger the minimum. Three or four staged elements carry the effect; staging ten adds time without adding meaning.
  • Never delay essential feedback. A button's pressed state, a focus ring and a validation message should appear immediately; sequence only decorative or secondary changes.
  • Remove delays with motion. For reduced-motion users, a delay without motion is just a wait.
  • Test with fast input. Sweep the pointer across a grid of choreographed cards. If the sequences pile up into visual noise, shorten the steps or stage fewer elements; interrupted sequences should read as calm, not frantic.

Browser support

transition-delay, including per-item delays in comma-separated lists and negative delays, is supported in every browser with CSS transitions, which dates from Chrome 26, Edge 12, Firefox 16 and Safari 9. Custom properties and calc() in time values are supported in all current engines, as are the standalone translate and scale properties (Chrome and Edge 104+, Firefox 72+, Safari 14.1+).

FAQ

What does transition-delay do? It postpones the start of a transition after the state change that triggers it. During the delay the property keeps its old value, then transitions over its duration. A negative delay starts the transition partway through, as if it had begun earlier.

How do I delay only the reverse of a transition? Declare different transition values on each state. The state being entered supplies the timing, so a delay on the base rule applies only when leaving the hovered or open state, and a delay on the hover rule applies only when entering it.

Can transition-delay stagger a list of items? Yes. Give each item a delay based on its index, usually through a custom property such as --i set per item, and write transition-delay: calc(var(--i) * 40ms). Cap the stagger so long lists do not take seconds to finish.

Should delays be removed for reduced motion? Usually yes. When movement is removed for reduced-motion users, a delay becomes pure waiting. Set delays to zero along with movement durations in the reduced-motion media query.

Related articles

More pages in the same section.