A CSS transition is one of the most powerful tools in a front-end developer's toolkit. When a CSS property changes value — because the user hovered over an element, a class was toggled via JavaScript, or an input received focus — the browser normally makes that change instantaneously. A transition intercepts that change and animates from the old value to the new one over a defined duration, producing smooth, professional-feeling UI feedback without a single line of animation JavaScript.
The mental model is straightforward: you declare on an element which properties should animate, how long they should take, what easing curve they should follow, and whether there should be a delay before they start. The browser handles all the frame-by-frame interpolation automatically, using the GPU for composited properties like transform and opacity — which is why those two are the gold standard for performant animation.
The shorthand transition property combines four sub-properties into a single declaration. You can target multiple properties by comma-separating them, and each comma-separated value can have its own duration, easing, and delay. This is far preferable to using transition: all, which forces the browser to check every CSS property on every frame — a performance trap that causes unnecessary paint work even for properties you never change.
The four sub-properties are transition-property (which CSS value to animate), transition-duration (how long), transition-timing-function (the easing curve), and transition-delay (how long to wait before starting). The delay is particularly useful for staggering effects — you can make a list of items appear one after another by giving each a progressively longer delay, all using the same base CSS class.
If duration determines how long an animation takes, the timing function determines how it feels. Two animations with identical duration but different timing functions feel completely different. ease-out — which starts fast and slows down — mimics the physical behaviour of an object arriving and settling, which is why it feels natural for elements entering the screen. ease-in — which starts slow and accelerates — mirrors an object building momentum before leaving, making it right for exits. ease-in-out is symmetric and works well for transitions between two equal states like expanding a panel. linear feels mechanical and robotic, which is actually what you want for things like progress bars that represent real measured progress rather than a motion effect.
The keyword shortcuts are just convenient aliases for cubic-bezier() values. The real power comes when you define your own curve — a spring-like overshoot using a Y value above 1.0, or an anticipation effect using a negative Y value. The Cubic Bezier tool on this site lets you build and preview any custom curve.
Not all CSS properties support interpolation. A property must have values the browser can calculate intermediate steps between. Numeric values, colors, lengths, and percentages are all interpolatable. Discrete values like display, visibility (partially), and font-family are not — the browser cannot compute a value halfway between block and none, for example.
Performance matters as much as support. The browser's rendering pipeline has three stages: layout, paint, and composite. Triggering layout reflow on every frame — which happens when you transition width, height, top, or left — is expensive and causes jank on lower-powered devices. Triggering paint — which happens with background-color, box-shadow, or border-color — is acceptable but still heavier than ideal. Only transform and opacity are handled entirely by the GPU compositor layer, which means they animate at full frame rate with zero impact on layout or paint. This is why good animation practice almost always means expressing motion through transform: translate() rather than changing position properties, and presence/absence through opacity rather than display.
One of the most common transition challenges is making an element disappear smoothly. Setting display: none removes it from the document instantly — you cannot transition to or from it. The solution is to combine opacity with visibility. The visibility property accepts a transition delay, so you can keep the element visually opaque until the opacity fade is complete, then make it non-interactive. On show, both transitions fire immediately with no delay. On hide, the opacity transition runs first, and the visibility change fires only after it finishes.
Staggering — making multiple elements animate sequentially rather than all at once — creates a more dynamic, alive feeling in a UI. The technique is simple: apply the same base transition to a set of elements, then give each element a progressively longer transition-delay using :nth-child selectors. When the parent class is toggled, the children animate one after another. The key insight is that the delay on the reverse (hide) direction should typically be the inverse order — the last item should leave first — which requires either reversing the delays on the closed state or using JavaScript to remove the class and let the browser reverse naturally.
A negative transition-delay value tells the browser to start the transition as if it had already been running for that duration. A value of -300ms means the animation starts at the 300ms mark rather than at 0. This is useful for loading states where you want an element to appear already in motion, or for creating the impression that a sequence started before the page loaded.
FLIP (First, Last, Invert, Play) is a pattern for animating layout changes that would otherwise require transitioning expensive properties. You record the element's position before the change (First), apply the change (Last), calculate the difference and apply the inverse as a transform (Invert), then remove the inverse so the browser animates to the final position (Play). The result is a smooth GPU-composited transform animation even though the underlying layout changed. This is how frameworks like React Spring and Framer Motion achieve smooth list reordering animations.
The will-change property is a hint to the browser that an element is about to animate, giving it the opportunity to promote that element to its own GPU compositing layer before the animation starts. This eliminates the promotion delay that can cause the first frame of an animation to appear slightly late. The property should be used sparingly — each promoted layer consumes GPU memory, and promoting everything defeats the purpose. Apply it only to elements that animate frequently or on user interaction, and remove it programmatically after the animation ends via a transitionend event listener.
Users with vestibular disorders, migraines, or motion sensitivity may experience genuine physical discomfort from on-screen animations. Operating systems expose a "Reduce Motion" accessibility setting, which is queryable via the prefers-reduced-motion media query. WCAG 2.1 Success Criterion 2.3.3 requires that motion triggered by interaction can be disabled. The simplest implementation is to zero out all transition and animation durations for users who prefer reduced motion, effectively making changes instantaneous. More thoughtful implementations preserve subtle opacity transitions (which most reduced-motion users tolerate) while removing spatial motion.
Transitions and CSS animations solve different problems. Transitions are always triggered by a state change and animate between exactly two states — the initial and the final value. They automatically reverse when the state change reverses. They require no @keyframes declaration. CSS animations, by contrast, can loop, can define multiple intermediate keyframes, can run automatically on page load without any interaction trigger, and can use animation-fill-mode to hold the final state after finishing. The practical rule: use transitions for UI state feedback (hover effects, focus rings, toggle states, drawer opens) and use CSS animations for anything that needs to run automatically, loop, or pass through more than two states.
The four most common causes: (1) The transition declaration is on the hover/active state rather than the base element — the base element must declare the transition for it to reverse smoothly. (2) The property is not interpolatable — display, z-index on non-positioned elements, and gradient background-image values cannot transition. (3) Both states have identical computed values. (4) The transition-duration is missing or defaults to 0s.
Both delay the start of the animation, but transition-delay delays the transition from triggering (e.g. from hover until the animation starts), while animation-delay delays the start of the keyframe animation from when the element loads or the class is applied. Negative values work in both — a negative transition-delay starts the transition partway through, and a negative animation-delay starts the animation as if it had already been running for that duration.
No — CSS gradient values are technically background-image values, and image values are not interpolatable. The browser cannot compute a halfway point between two gradient declarations. The workaround: stack a pseudo-element with the new gradient behind or above the current background, and transition its opacity from 0 to 1. This gives the visual effect of a gradient transitioning without actually interpolating gradient values.
You cannot — display changes are instantaneous. Use the opacity and visibility pattern: set opacity: 0; visibility: hidden and transition both, with a matching delay on visibility for the hide direction so the element doesn't become non-interactive until after it fades out. For height-based collapsing, use max-height: 0 transitioning to a max-height larger than the content.
Google's Material Design 3 guidelines recommend 100–200ms for small component state changes (button presses, toggles, focus rings), 250–400ms for medium transitions (cards expanding, sheets sliding), and 400–600ms for full-screen transitions. Anything under 100ms is imperceptible. Anything over 500ms starts feeling sluggish for repeated interactions like hovering multiple buttons in a row. Always test at reduced motion for users who need it.
CSS transitions are ideal for discrete state changes driven by class or attribute changes — hover effects, focus rings, toggles, drawer slides, modal opens. JavaScript animation libraries like GSAP or Motion One are better when you need sequencing across multiple elements, physics simulations, complex orchestration, or scroll-linked animations. For most UI polish work, CSS transitions are simpler, more performant, and require no dependencies.