CSS transitions are the simplest way to add motion to a web interface. With a single property you can make any CSS change — a color, a size, a position — animate smoothly instead of snapping instantly. Transitions are lightweight, GPU-accelerated, and require zero JavaScript for the most common use cases: hover effects, focus states, and toggle animations.
This guide covers everything you need: the four transition sub-properties, every timing function explained with examples, which CSS properties are safe to animate, and performance best practices so your transitions never cause jank.
The transition Property Syntax
The transition shorthand takes up to four values:
/* property | duration | easing | delay */
.element {
transition: background-color 0.3s ease 0s;
}
/* Multiple properties */
.element {
transition: transform 0.25s ease, opacity 0.25s ease;
}
/* Transition everything (use with care) */
.element {
transition: all 0.25s ease;
}
transition-property
Specifies which CSS properties to animate. You can list specific properties separated by commas, or use all to animate every animatable property. Using specific properties is preferred — it's more performant and makes your intent clear to other developers.
transition-duration
How long the animation takes, in seconds (s) or milliseconds (ms). For UI interactions, 150ms–300ms feels natural. Durations over 500ms start to feel sluggish for hover effects but can work for page-level transitions. Durations under 100ms can feel too abrupt.
transition-timing-function (Easing)
Controls the acceleration curve — how fast the animation goes at different points in time. This is what makes a transition feel bouncy, snappy, or mechanical. CSS provides five keywords and the cubic-bezier() function for custom curves:
/* Built-in keywords */ transition-timing-function: ease; /* fast start, slow finish — most natural */ transition-timing-function: ease-in; /* slow start, fast finish */ transition-timing-function: ease-out; /* fast start, slow finish (abrupt start) */ transition-timing-function: ease-in-out; /* slow start AND slow finish — very smooth */ transition-timing-function: linear; /* constant speed — mechanical feel */ /* Custom cubic bezier — create at cubic-bezier.com */ transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* overshoot bounce */
Choosing easing: Use ease for most hover effects. Use ease-out for elements entering the screen (they decelerate to a stop). Use ease-in for elements leaving the screen (they accelerate away). For dismiss/close actions, ease-in with a shorter duration (150ms) feels snappy and responsive.
transition-delay
Delays the start of the transition. Useful for sequencing multiple elements or revealing content in stages. Negative delays start the animation partway through — a technique for staggered list entrance animations.
Common Transition Patterns
Hover Lift Card
The most common UI pattern: a card that lifts on hover. Always transition transform and box-shadow together — the shadow amplifies the sense of depth:
.card {
border-radius: 12px;
background: var(--surface);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2);
}
Button Color Transition
Smooth background-color transitions on buttons feel polished. Never use all on interactive elements — it can accidentally transition properties you don't want animated (like width during text reflow):
.btn {
background: #7c6fff;
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.18s ease, transform 0.18s ease;
}
.btn:hover {
background: #6456e0;
transform: scale(1.02);
}
.btn:active {
transform: scale(0.97);
}
Fade In/Out with display:none
CSS transitions cannot animate from or to display: none directly — the element snaps in or out. The reliable workaround is animating opacity and visibility together, or using height: 0 with overflow: hidden for collapsed content:
/* Fade with visibility */
.tooltip {
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
.tooltip.visible {
opacity: 1;
visibility: visible;
}
/* Collapse with height */
.panel {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.panel.open {
max-height: 500px; /* must be larger than content */
}
The max-height trick caveat: The transition duration is calculated against the max-height value, not the actual content height. If your content is 50px but max-height is 500px, the animation will feel slow (it's spending 90% of its time going through invisible height). Set max-height to a reasonable maximum, not an extreme.
Navigation Link Underline
A classic: animated underline on nav links using a pseudo-element and transform: scaleX(). This is more flexible than text-decoration and fully controllable:
.nav-link {
position: relative;
text-decoration: none;
color: var(--text);
}
.nav-link::after {
content: '';
position: absolute;
left: 0;
bottom: -2px;
width: 100%;
height: 2px;
background: linear-gradient(90deg, #7c6fff, #ff6fb0);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.25s ease;
}
.nav-link:hover::after {
transform: scaleX(1);
transform-origin: left;
}
What Can CSS Transition?
Not every CSS property is animatable. Properties that have intermediate numerical states can transition; discrete properties (like display or font-family) cannot. Safe, commonly transitioned properties include:
- Transform —
translateX/Y,scale,rotate,skew— GPU-accelerated ✓ - Opacity — GPU-accelerated ✓
- Color properties —
color,background-color,border-color - Box model —
width,height,padding,margin— triggers layout (use sparingly) - Shadows —
box-shadow,text-shadow - Border radius —
border-radius - Filter —
blur,brightness,saturate
Performance: What to Animate and What to Avoid
Only two CSS properties are truly free to animate: transform and opacity. These run on the GPU compositor thread and never trigger layout or paint. Animating anything else — width, height, top, left, margin, padding, background-color — forces the browser to recalculate layout or repaint pixels on the CPU, which can drop below 60fps on complex pages.
/* ✓ Compositor-only — always smooth */
.element { transition: transform 0.25s ease, opacity 0.25s ease; }
/* ⚠ Triggers repaint — fine for simple pages, avoid on complex ones */
.element { transition: background-color 0.25s ease, box-shadow 0.25s ease; }
/* ✗ Triggers layout — never animate these */
.element { transition: width 0.25s ease, height 0.25s ease; }
/* Prefer: transition: transform 0.25s ease; + scale() instead */
Build transforms visually
Use our CSS Transform Generator to dial in rotateX, scale, skew and more — then copy the code.
Transitions vs. Animations
CSS transitions fire once between two states (usually a default state and a hover/focus/active state). CSS @keyframes animations can loop, chain multiple steps, run automatically without user interaction, and control fill modes. Use transitions for reactions to user input; use @keyframes for autonomous, repeating, or multi-step motion. See our CSS Animation Generator for keyframe-based animations, or read the CSS animation performance guide for a deeper comparison.
Reducing Motion for Accessibility
Always respect the prefers-reduced-motion media query. Users who set this preference in their OS have explicitly said they want less motion — ignoring it can cause discomfort or harm for people with vestibular disorders:
/* Base transitions */
.card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
/* Disable for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
/* Or: provide a non-motion fallback */
.card:hover {
box-shadow: 0 0 0 2px var(--accent); /* highlight without movement */
}
}
Frequently Asked Questions
Why isn't my CSS transition working?
display. (2) The transition is on the wrong selector — it must be on the element in its normal state, not only on the :hover state (if only on hover, it'll animate in but snap back). (3) The property value isn't changing — check that your hover/active state is actually applying a different value.Why does my transition only work in one direction?
transition only on the :hover rule, the animation plays when hovering in but snaps instantly when hovering out. Put transition on the base element selector to get smooth animation in both directions.Can I trigger a CSS transition with JavaScript?
element.style.setProperty() to change custom properties and trigger transitions that way. Listen for the transitionend event to run code after the transition finishes.What is the difference between CSS transition and animation?
@keyframes to define multiple states and runs automatically, can loop, and can use fill modes to control what happens before and after. Transitions = reactive; animations = autonomous.How do I stagger transitions across multiple elements?
transition-delay with incrementing values on each child: .item:nth-child(1) { transition-delay: 0s; } .item:nth-child(2) { transition-delay: 0.05s; } etc. For long lists, generate these rules with a CSS loop in SCSS or with a JavaScript forEach setting inline style.transitionDelay.Ready to go deeper on motion? See our CSS Animation Generator for keyframe animations, or explore the CSS Transform Generator to build the transforms you want to transition between.