How Transitions Work
A transition watches for a CSS property to change (usually triggered by :hover, :focus, a class being added/removed via JavaScript) and animates that change over a specified duration instead of applying it instantly.
/* Apply transition on the ELEMENT, not on the :hover state */
.btn {
background-color: #1572B6;
transition: background-color 0.3s ease;
}
/* When this changes, it will animate over 0.3s */
.btn:hover {
background-color: #0d5fa0;
}
The Four Transition Properties
transition-property
Which CSS properties to animate. Only changes to listed properties will transition.
transition-property: all; /* all animatable properties */
transition-property: background-color; /* only bg color */
transition-property: transform, opacity; /* specific list */
transition-property: none; /* disable all transitions */
transition: all watches every property for changes and tries to animate them. This can animate properties you don't intend (like width during a layout change) and causes performance issues. Always list specific properties.
transition-duration
transition-duration: 0.3s; /* 300ms — good for color/opacity */
transition-duration: 200ms; /* same as 0.2s */
transition-duration: 0.5s; /* slower — for larger movements */
transition-duration: 0s; /* instant (override to disable) */
/* Rule of thumb for UI transitions:
100–200ms: micro-interactions (hover, focus ring)
200–400ms: element appearing/disappearing, size changes
400–600ms: page sections sliding in
600ms+: usually too slow — feels sluggish */
transition-timing-function
Controls the acceleration curve — how fast the transition moves at each point.
/* Named keywords */
transition-timing-function: ease; /* slow start, fast middle, slow end (default) */
transition-timing-function: linear; /* constant speed — use for spinners */
transition-timing-function: ease-in; /* slow start, fast end — things leaving */
transition-timing-function: ease-out; /* fast start, slow end — things arriving */
transition-timing-function: ease-in-out; /* slow start and end — balanced */
/* cubic-bezier() — full control over the curve */
/* cubic-bezier(x1, y1, x2, y2) — values 0–1 for x, any for y */
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); /* Material Design standard */
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* spring overshoot */
/* steps() — discrete jumps (sprite animation) */
transition-timing-function: steps(4, end); /* 4 equal frames */
transition-timing-function: step-start; /* jump immediately */
transition-timing-function: step-end; /* jump at end */
transition-delay
transition-delay: 0s; /* no delay (default) */
transition-delay: 0.1s; /* wait 100ms before starting */
transition-delay: -0.2s; /* negative: starts 0.2s INTO the transition */
/* Stagger effect on multiple elements */
.item:nth-child(1) { transition-delay: 0s; }
.item:nth-child(2) { transition-delay: 0.05s; }
.item:nth-child(3) { transition-delay: 0.1s; }
.item:nth-child(4) { transition-delay: 0.15s; }
transition Shorthand
/* transition: property duration timing-function delay */
transition: background-color 0.3s ease 0s;
/* Multiple transitions — comma separated */
.card {
transition:
background-color 0.3s ease,
transform 0.25s ease-out,
box-shadow 0.3s ease,
opacity 0.2s ease;
}
/* Common real-world patterns */
.btn { transition: background-color 0.2s ease, transform 0.15s ease; }
.link { transition: color 0.2s ease; }
.overlay { transition: opacity 0.3s ease; }
.drawer { transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1); }
Animatable Properties – Performance Matters
Not all CSS properties are equal to animate. The browser's rendering pipeline determines performance:
/* ✅ BEST: Compositor-only — GPU, no layout or paint */
transition: transform 0.3s ease; /* translateX, scale, rotate, etc. */
transition: opacity 0.3s ease; /* fade in/out */
/* ⚠️ OK: Paint-only — triggers repaint, not reflow */
transition: color 0.2s ease;
transition: background-color 0.3s ease;
transition: border-color 0.2s ease;
transition: box-shadow 0.3s ease;
transition: outline 0.15s ease;
/* ❌ AVOID for frequent animation: triggers reflow */
/* These force the browser to recalculate layout every frame */
transition: width 0.3s ease; /* reflow */
transition: height 0.3s ease; /* reflow */
transition: margin 0.3s ease; /* reflow */
transition: padding 0.3s ease; /* reflow */
transition: top 0.3s ease; /* reflow — use transform: translateY() instead */
transition: left 0.3s ease; /* reflow — use transform: translateX() instead */
transition: font-size 0.3s ease; /* reflow */
/* Rule: use transform instead of position/size for movement */
/* SLOW: */ .modal { left: -100%; } .modal.open { left: 0; }
/* FAST: */ .modal { transform: translateX(-100%); } .modal.open { transform: translateX(0); }
Real-World Patterns
/* 1. Button hover */
.btn {
background-color: #1572B6;
transform: translateY(0);
box-shadow: 0 2px 8px rgba(0,0,0,.15);
transition: background-color 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
}
.btn:hover {
background-color: #0d5fa0;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,.2);
}
.btn:active { transform: translateY(0); }
/* 2. Fade in/out */
.tooltip { opacity: 0; pointer-events: none; transition: opacity 0.2s ease; }
.tooltip.visible { opacity: 1; pointer-events: auto; }
/* 3. Slide drawer */
.drawer {
transform: translateX(-100%);
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.drawer.open { transform: translateX(0); }
/* 4. Card lift on hover */
.card {
box-shadow: 0 2px 8px rgba(0,0,0,.1);
transform: translateY(0);
transition: box-shadow 0.3s ease, transform 0.25s ease;
}
.card:hover {
box-shadow: 0 12px 32px rgba(0,0,0,.18);
transform: translateY(-4px);
}
/* 5. Smooth focus ring */
.input {
outline: 2px solid transparent;
outline-offset: 2px;
transition: outline-color 0.15s ease;
}
.input:focus { outline-color: #1572B6; }
/* 6. Expand/collapse height (needs known max-height) */
.panel { max-height: 0; overflow: hidden; transition: max-height 0.4s ease; }
.panel.open { max-height: 500px; }
/* 7. Accessibility: respect reduced motion */
@media (prefers-reduced-motion: reduce) {
* { transition-duration: 0.01ms !important; }
}
📋 Summary
- Put
transitionon the element, not the trigger state (:hover). - transition: property duration timing delay — all four parts.
- Multiple transitions: comma-separate them.
- Avoid
transition: all— list specific properties. - Best performers:
transformandopacity— compositor only, 60fps. - Good:
color,background-color,box-shadow— repaint only. - Avoid for animation:
width,height,top,left,margin— trigger reflow. - UI durations: 100–200ms micro, 200–400ms element changes, 400–600ms section transitions.
- Always add
@media (prefers-reduced-motion: reduce)override.
Frequently Asked Questions
Most common causes: (1) transition is on the :hover state instead of the element itself — the transition must be on the base element. (2) The property is not animatable — display, visibility, and content cannot transition. (3) display: none to display: block cannot be transitioned — use opacity + visibility or max-height instead. (4) The starting and ending values are the same. Check all four in DevTools.
display is not animatable — it switches instantly. Use this pattern instead: opacity: 0; visibility: hidden; pointer-events: none; → opacity: 1; visibility: visible; pointer-events: auto; with transition: opacity 0.3s ease, visibility 0.3s ease. The visibility transition keeps the element in layout but invisible, while pointer-events: none prevents clicks. Modern CSS also supports @starting-style for transitioning from display: none in Chrome 116+.
Transitions require a trigger — a state change (hover, focus, class toggle). They go from A to B once. Animations (@keyframes) run independently — they can loop, run automatically on load, go through multiple steps (A→B→C→D), and play in reverse. Use transitions for interactive state changes (hover effects, UI feedback). Use animations for autonomous motion (loading spinners, entrance effects, looping decorative elements).
Objects in the physical world decelerate as they arrive — they don't slam into their final position. ease-out (fast start, slow end) mimics this natural deceleration, which feels more physical and comfortable. ease-in (slow start, fast end) feels like something flying out of frame — better for elements leaving the screen. For most UI elements entering the viewport or revealing themselves: use ease-out. For elements departing: use ease-in.