a transition is three knobs you compose
A Tailwind transition is not one utility — it's three independent
CSS properties composed together. Get any one wrong and the effect breaks
in a confusing way. WHICH properties animate
(transition-colors), HOW LONG
(duration-300), and the SHAPE
of the curve (ease-out). Optionally a fourth: WHEN
(delay-150) and a PERF HINT (will-change-transform).
| knob | tailwind utility | css property | default if omitted |
|---|---|---|---|
| which properties | transition · transition-colors · transition-transform · transition-opacity · transition-shadow · transition-none |
transition-property |
transition-property: none → nothing animates |
| how long | duration-150 · duration-300 · duration-700 · duration-[250ms] |
transition-duration |
0s → snaps instantly (even with property set) |
| shape of curve | ease-linear · ease-in · ease-out · ease-in-out · ease-[cubic-bezier(0.4,0,0.2,1)] |
transition-timing-function |
ease (= cubic-bezier(0.25,0.1,0.25,1)) |
| when to start | delay-150 · delay-300 · delay-[1s] |
transition-delay |
0s (starts immediately) |
| perf hint | will-change-auto · will-change-transform · will-change-[opacity] |
will-change |
auto (browser guesses; usually fine) |
Plain transition (no suffix) is the "common set":
color, background-color, border-color, outline-color, text-decoration-color,
fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter.
It's the safe default. transition-all uses the literal
token all — heavy and discouraged: it can thrash layout when
an unrelated property (e.g. width) changes mid-transition.
1 · hover buttons — which properties animate
Same hover target on each card (bg color change + scale + fade), but each
button allows only ONE property group to animate. Hover each one and watch
what moves vs. what snaps. The transition-none card is the
control: everything snaps.
The first three cards each show ONE thing animating while the other two
hover effects snap. The fourth (transition) animates all
three because transform, opacity, and the
color properties are all in the common set.
2 · duration race — 150ms vs 300ms vs 700ms
Click Play to send all three runners across their tracks at once.
They use the same easing (ease-linear) but different
duration-*. The 150ms runner finishes first; 700ms finishes
last. This is the knob that controls perceived speed.
click Play to start the race
Default gotcha: if you forget duration-*, the duration
is 0s and the runner snaps to the end with no visible
transition — even though transition-transform is set. Many
"my transition doesn't work" bugs are a missing duration.
3 · easing comparison — shape of the curve
Same duration (duration-700) for all four runners, but each
uses a different transition-timing-function. Linear
moves at constant speed; ease-in starts slow; ease-out ends
slow (feels natural for UI enter); ease-in-out is slow at both
ends. The custom cubic-bezier(0.34,1.56,...) overshoots.
click Play to start the race
Rule of thumb: entering UI elements use ease-out or an
overshoot bezier; exiting elements use ease-in. ease-in-out
is for state toggles that go both directions. Avoid ease-linear
for organic motion — it reads as mechanical.
4 · delay stagger — same animation, sequenced start
delay-* offsets the start of a transition (not the
duration). With the same duration and easing, increasing delays create a
staggered "cascade" — the classic list-reveal or menu-open feel. Click
Play to see all five runners leave the start line in sequence.
click Play to start the cascade
Tip: for long lists, compute the delay per item with inline
style="transition-delay: calc(var(--i) * 50ms)" rather than
authoring delay-50 … delay-500 by hand. For
looping/entrance animations, prefer
keyframes + animation-delay.
intent → pattern
| intent | pattern | why |
|---|---|---|
| smooth color/bg/border hover | transition-colors duration-200 |
scoped to color props — cheapest, no layout/paint thrash |
| card lift on hover | transition-transform duration-300 ease-out + hover:-translate-y-1 |
transform is GPU-composited; ease-out feels like it "settles" |
| fade in/out (modal, tooltip) | transition-opacity duration-200 |
only opacity animates — pairing with pointer-events-none on hidden |
| spring/overshoot pop | transition duration-500 ease-[cubic-bezier(0.34,1.56,0.64,1)] |
2nd control-y > 1 → overshoots past 100% then settles |
| staggered list reveal | transition duration-300 ease-out + per-item delay-75 … delay-300 |
same animation, offset starts — feels designed, not mechanical |
| disable a transition conditionally | transition-none (or media query: motion-reduce:transition-none) |
respect prefers-reduced-motion for accessibility |
| perf hint for a known-heavy animation | will-change-transform on the element |
only set it right before & remove after — permanent will-change wastes RAM |
| custom duration/easing tokens | @theme { --ease-spring: cubic-bezier(0.34,1.56,0.64,1); } → ease-spring |
v4 reads timing-function + duration tokens from @theme |