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.
Step-by-step resolution
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 foropacity. Sequencing moves the two costs into different frames, so the expensive width change is never competing with the fade. For a compositor-only sequence, animatescaleon 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.
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.
Related
- CSS Transitions vs Animations — the parent topic
- Interrupting and Reversing Transitions Mid-Flight — per-property retargeting rules
- Choreographing Multi-Element Motion — sequencing across components