Chaining Transitions with transition-delay

Part of CSS Transitions vs Animations in Core CSS Animation Fundamentals.

The problem

A search field expands in two steps: first it widens, then its placeholder and clear button fade in. When it collapses, the order should reverse — contents fade out first, then the field narrows. The first implementation used keyframe animations for both directions, which restart from their first keyframe if the user clicks again mid-sequence. The second used setTimeout to add classes in order, which drifts out of sync with the CSS durations and breaks under reduced motion.

Transitions can express sequences directly, because every property in a transition list has its own delay. And the transition that runs is the one declared on the state being entered, which lets each direction have its own order.

Root cause analysis: the destination state owns the timing

When a property changes, the browser uses the transition-* values from the new computed style — the style of the state being entered. That rule has a useful consequence: the open state’s transition list controls how the component opens, and the closed state’s list controls how it closes. They do not need to match.

Per-property delays sequence the steps. In transition: inline-size 200ms, opacity 150ms 200ms, the width starts immediately and the opacity waits 200ms. Each entry’s delay is measured from the moment of the state change.

Reversing uses the reverse list. Declaring transition: opacity 120ms, inline-size 200ms 120ms on the closed state makes the contents fade before the field narrows. The same element, the same properties, a different order per direction.

Interruptions retarget with delays applied again. If the user reopens during the close, a new transition starts for each property from its current value, using the open state’s delays. A property that is still delayed in the close list simply never started; a property already moving reverses. The result is usually acceptable, but it can include a short pause while a delayed property waits its turn again — the interruption rules apply per property.

Negative delays start part-way. transition-delay: -100ms starts the transition as if 100ms had already elapsed. It is useful for syncing a late-joining property with one already running, rarely for sequencing.

Search field open and close sequencesEach direction declares its own order on the state it enters.Search field open and close sequencesOpen: inline-sizewidendone350 msOpen: opacitywaitfade in350 msClose: opacityfade outdone320 msClose: inline-sizewaitnarrow320 mswidendonewaitfade infade outnarrow
Each direction declares its own order on the state it enters.

Step-by-step resolution

Building a two-direction sequenceTwo transition lists, one per state; no timers.Building a two-direction sequence1Write the open state's properties and a transition list in opening order with cumulativedelays.Opening sequence2Write the closed state's transition list in reverse order with its own delays.Closing sequence3Keep the whole sequence under about 400 ms.The UI stays responsive4Toggle rapidly and watch for unacceptable pauses.Interruption behaviour checked5If mid-sequence reversal must be seamless, switch to the Web Animations API.Timeline-level control6Under reduced motion, set every delay to zero.
Two transition lists, one per state; no timers.

Production code pattern

.search {
  inline-size: 2.5rem;
  overflow: clip;
  /* CLOSING order (this is the state being entered on close): contents out, then narrow. */
  transition:
    inline-size 200ms cubic-bezier(0.4, 0, 0.2, 1) 120ms;
}
.search__input,
.search__clear {
  opacity: 0;
  transition: opacity 120ms ease-in;
}

.search[data-open] {
  inline-size: 16rem;
  /* OPENING order: widen first. */
  transition: inline-size 200ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.search[data-open] .search__input,
.search[data-open] .search__clear {
  opacity: 1;
  /* ...then contents fade in once the field has room. */
  transition: opacity 150ms ease-out 200ms;
}

@media (prefers-reduced-motion: reduce) {
  .search, .search[data-open] { transition: none; }
  .search__input, .search__clear,
  .search[data-open] .search__input,
  .search[data-open] .search__clear { transition: opacity 100ms linear; }
}

Rendering Impact: layout per frame for inline-size, composite for opacity. Sequencing moves the two costs into different frames, so the expensive width change is never competing with the fade. For a compositor-only sequence, animate scale on a wrapper instead of width.

The comments in the rules are not decoration. Because the destination state owns the timing, the transition on .search describes how the field closes, which reads backwards until you know the rule. Labelling each list with its direction prevents someone “fixing” it later.

Timers against per-property delaysBoth produce a sequence; only one stays in sync with CSS.Timers against per-property delayssetTimeout adds classes in orderDurations duplicated in scriptReduced motion still waitsRapid toggles queue stale timersDrifts and misfirestransition-delay per propertyTiming lives in one stylesheetReduced motion sets delays to zeroToggles retarget from current valuesDeclarative and consistent
Both produce a sequence; only one stays in sync with CSS.

When to switch to keyframes or script

Delays sequence properties, not elements in a complex choreography. Once a sequence involves more than two or three steps, different elements with dependencies between them, or needs to pause and resume, transitions become hard to read. Keyframe animations with percentage stops suit fixed choreographies that are not interrupted; the Web Animations API with finished promises, as in coordinating sequential animations with promises, suits sequences that must reverse seamlessly or react to events between steps.

Verification checklist

Constraints and trade-offs

  • Delays make the interface feel slower; each step adds latency before the final state.
  • Interrupting during a delay restarts that property’s delay from the new state’s list.
  • Layout properties in a sequence still cost layout per frame during their step.
  • Focus should move when the component is usable, not after the full sequence.
  • Long cumulative delays are hard to maintain; use tokens for step durations.

Frequently asked questions

Which state’s transition values are used?

The state being entered. The open state’s transition list controls opening and the closed state’s list controls closing, so each direction can have its own order.

Can transition-delay be different for each property?

Yes. Each entry in a comma-separated transition list has its own duration, easing and delay.

What happens if the user toggles during the delay?

A new transition starts from each property’s current value using the new state’s delays, so a property that had not started simply waits according to the new list.

When should I use keyframes instead?

For fixed, multi-element choreographies that are not interrupted. For sequences that must reverse seamlessly or pause, use the Web Animations API.