CSS Transitions
Smoothly animate between two CSS states with a single property.
1. Introduction
When a button changes color on hover, or a dropdown slides open, that smooth movement is usually a CSS transition. A transition tells the browser: "whenever this CSS property changes, animate the change over a given duration instead of jumping instantly."
Transitions are the simplest form of CSS animation — they animate between exactly two states (the default state and the triggered state). You do not write any frames or define sequences. Just specify which property, how long, and what easing curve.
In this lesson you will learn:
- The four transition sub-properties and the shorthand
- Easing functions (timing functions) and how they feel
- Which properties you can and cannot transition
- How to transition multiple properties at once
- Performance considerations — why
transformandopacityare special
2. Theory
2.1 The four sub-properties
| Property | What it controls | Default |
|---|---|---|
transition-property | Which CSS property to animate (all or a specific name) | all |
transition-duration | How long the transition takes (e.g., 0.3s, 200ms) | 0s |
transition-timing-function | The acceleration curve (easing) | ease |
transition-delay | How long to wait before starting | 0s |
2.2 The shorthand
/* transition: property duration timing-function delay */
.btn {
transition: background-color 0.3s ease 0s;
}
/* Multiple properties — comma-separated */
.card {
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
2.3 Where to put the transition rule
Always put transition on the base state (the element itself), not on the hover/focus state. This way the transition plays in both directions — going in and coming out.
/* Good — transition on base state, direction plays both ways */
.btn {
background-color: royalblue;
transition: background-color 0.3s ease;
}
.btn:hover {
background-color: navy;
}
/* Bad — only transitions in, jumps back instantly */
.btn { background-color: royalblue; }
.btn:hover {
background-color: navy;
transition: background-color 0.3s ease;
}
2.4 Timing functions
| Value | Feel | Use when |
|---|---|---|
ease | Starts fast, ends slow (default) | General UI interactions |
linear | Constant speed | Spinners, progress bars |
ease-in | Starts slow, ends fast | Elements leaving the screen |
ease-out | Starts fast, ends slow | Elements entering the screen |
ease-in-out | Slow at both ends | Back-and-forth / toggles |
cubic-bezier(x1,y1,x2,y2) | Custom curve | Fine-tuned motion design |
steps(n) | Jumps in n discrete steps | Sprite animations, loading dots |
2.5 Which properties can be transitioned?
Only properties with numeric or colour values can be transitioned. The browser needs a start value and an end value to interpolate between them.
Transitionable: color, background-color, opacity, transform, width, height, top, left, border-radius, box-shadow, font-size, padding, margin, grid-template-columns (partially)...
Not transitionable: display, visibility (jumps between values), background-image (different URL), font-family, content.
Avoid transition: all in production — it animates every changing property (including layout-triggering ones) and can hurt performance.
2.6 Performance — transform and opacity
The browser rendering pipeline has three stages: layout (size/position) → paint (pixels) → composite (layers). Layout changes are the most expensive.
transformandopacityrun on the compositor thread — GPU-accelerated, 60 fps even on slow devices.- Avoid transitioning
width,height,top,left,margin,padding— these cause layout recalculation ("layout thrashing").
/* Avoid — triggers layout */
.box:hover { left: 20px; }
/* Prefer — compositor only */
.box:hover { transform: translateX(20px); }
2.7 The transition shorthand with multiple properties
.nav-link {
color: #333;
border-bottom: 2px solid transparent;
/* Transition color and border separately with different durations */
transition: color 0.2s ease, border-bottom-color 0.3s ease 0.05s;
}
.nav-link:hover {
color: royalblue;
border-bottom-color: royalblue;
}
2.8 Using transition with JavaScript-toggled classes
Transitions work with any CSS state change — not just pseudo-classes. Adding or removing a class with JavaScript also triggers transitions:
/* CSS */
.sidebar {
transform: translateX(-100%);
transition: transform 0.35s ease-out;
}
.sidebar.is-open {
transform: translateX(0);
}
/* JS */
document.querySelector('.menu-btn').addEventListener('click', () => {
document.querySelector('.sidebar').classList.toggle('is-open');
});
3. Real World Example
Transitions appear everywhere in professional interfaces:
- Buttons — background color, box-shadow, and scale change on hover/active.
- Navigation — underline slides in beneath the active link.
- Dropdowns / off-canvas menus — translate into view from off-screen.
- Form inputs — border color and box-shadow transition on focus.
- Cards — subtle lift (box-shadow + translateY) on hover.
- Modals — opacity fades in while backdrop fades in simultaneously.
- Tooltips — opacity + scale transition from invisible to visible.
4. Code Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CSS Transitions Demo</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; padding: 2rem; background: #f5f5f5; }
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1.5rem;
max-width: 900px;
margin: 0 auto;
}
.demo-label { font-size: 0.8rem; color: #666; margin-bottom: 0.5rem; }
/* ---- 1. Button with background + transform ---- */
.btn-demo {
display: inline-block;
padding: 0.75rem 1.5rem;
background: royalblue;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
}
.btn-demo:hover {
background-color: #1a3dbf;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(65, 105, 225, 0.4);
}
.btn-demo:active {
transform: translateY(0);
box-shadow: none;
}
/* ---- 2. Card lift effect ---- */
.card-demo {
background: white;
border-radius: 10px;
padding: 1.25rem;
box-shadow: 0 2px 6px rgba(0,0,0,.1);
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.card-demo:hover {
transform: translateY(-6px);
box-shadow: 0 10px 30px rgba(0,0,0,.15);
}
/* ---- 3. Input focus ring ---- */
.input-demo {
width: 100%;
padding: 0.6rem 0.8rem;
border: 2px solid #ccc;
border-radius: 6px;
font-size: 1rem;
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.input-demo:focus {
border-color: royalblue;
box-shadow: 0 0 0 3px rgba(65,105,225,.25);
}
/* ---- 4. Underline nav link ---- */
.nav-link-demo {
display: inline-block;
text-decoration: none;
color: #333;
padding-bottom: 4px;
border-bottom: 2px solid transparent;
transition: color 0.2s ease, border-bottom-color 0.2s ease;
}
.nav-link-demo:hover {
color: royalblue;
border-bottom-color: royalblue;
}
/* ---- 5. Toggled sidebar ---- */
.toggle-wrap { position: relative; height: 80px; overflow: hidden; background: #eee; border-radius: 8px; }
.panel {
position: absolute;
top: 0; left: 0;
width: 200px; height: 100%;
background: royalblue;
color: white;
display: flex; align-items: center; justify-content: center;
transform: translateX(-100%);
transition: transform 0.35s ease-out;
}
.panel.is-open { transform: translateX(0); }
.open-btn {
position: absolute; top: 50%; right: 1rem;
transform: translateY(-50%);
background: #333; color: white; border: none; padding: 0.4rem 0.8rem;
border-radius: 4px; cursor: pointer;
}
</style>
</head>
<body>
<div class="demo-grid">
<div>
<p class="demo-label">Button (hover me)</p>
<button class="btn-demo">Click Me</button>
</div>
<div class="card-demo">
<p class="demo-label">Card lift (hover me)</p>
<p>Hover to see the elevation effect.</p>
</div>
<div>
<p class="demo-label">Input focus</p>
<input class="input-demo" type="text" placeholder="Click to focus">
</div>
<div>
<p class="demo-label">Nav link underline</p>
<a class="nav-link-demo" href="#">About Us</a>
</div>
<div class="toggle-wrap">
<div class="panel" id="panel">Sidebar Panel</div>
<button class="open-btn" onclick="document.getElementById('panel').classList.toggle('is-open')">Toggle</button>
</div>
</div>
</body>
</html>
5. Code Breakdown
Button transition (lines 20–25)
transition: background-color 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease — three separate properties with different durations to keep the hover feeling snappy yet polished. transform: translateY(-2px) gives a subtle float effect without touching layout.
Card lift (lines 31–39)
A classic pattern: on hover, the card moves up 6 px (translateY(-6px)) and casts a deeper shadow. Because we use transform instead of top/margin-top, the layout of surrounding elements is unaffected and the animation is GPU-accelerated.
Input focus ring (lines 42–51)
outline: none removes the browser default, but we add an accessible focus ring via box-shadow. The semi-transparent ring (rgba(65,105,225,.25)) communicates focus without being visually harsh. Both border-color and box-shadow transition at 0.2s.
Underline nav link (lines 54–63)
The trick is setting border-bottom: 2px solid transparent on the base state. This keeps the element height stable so the underline appearing does not shift surrounding content. On hover the border color transitions from transparent to royalblue.
Toggled panel (lines 66–78)
The panel starts at transform: translateX(-100%) — fully off-screen to the left. Toggling the is-open class changes it to translateX(0). Because transition: transform 0.35s ease-out is on the base state, the animation plays both when opening and closing.
6. Common Mistakes
Mistake 1 — Transitioning layout properties
/* Bad — triggers layout recalculation, janky on low-end devices */
.box:hover { width: 200px; }
/* Good — transform only composites, no layout impact */
.box:hover { transform: scaleX(1.5); }
Mistake 2 — Using transition: all
/* Bad — animates every changing property including layout triggers */
.card { transition: all 0.3s ease; }
/* Good — be explicit */
.card { transition: transform 0.3s ease, box-shadow 0.3s ease; }
Mistake 3 — Forgetting to transition on the base state
/* Bad — only transitions in, jumps back */
.btn:hover { background: navy; transition: background 0.3s; }
/* Good — on base state */
.btn { background: royalblue; transition: background 0.3s; }
.btn:hover { background: navy; }
Mistake 4 — Removing :focus-visible styles without replacement
Never use outline: none without providing an alternative focus indicator (like a box-shadow ring). Keyboard users depend on visible focus states.
Mistake 5 — Animating opacity but not handling visibility
/* Bad — element is invisible but still clickable */
.tooltip { opacity: 0; transition: opacity 0.2s; }
.tooltip.visible { opacity: 1; }
/* Better — combine with visibility */
.tooltip {
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0s 0.2s; /* delay visibility removal */
}
.tooltip.visible {
opacity: 1;
visibility: visible;
transition-delay: 0s;
}
Mistake 6 — Ignoring prefers-reduced-motion
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
7. Best Practices
- Put transition on the base state so it animates in both directions.
- Prefer transform and opacity over position/size properties for performance.
- Avoid transition: all — name the specific properties you want to animate.
- Keep durations short — UI interactions feel best at 150–350 ms. Use longer durations (400–600 ms) only for page-level transitions.
- Use ease or ease-out for elements entering the screen; ease-in for elements leaving.
- Respect prefers-reduced-motion — wrap or disable transitions for users who opt out.
- Provide a keyboard-accessible focus style — never remove
outlinewithout a replacement. - Use cubic-bezier() for custom easing — tools like easings.net let you design and preview curves visually.
- Combine opacity and visibility when hiding elements so they are not keyboard-focusable while invisible.
- Test on real devices — animations that feel smooth on a desktop can stutter on mid-range phones.
8. Practice Exercise
Build a polished interactive card component.
Requirements
- Create a product card with an image area, title, description, and a "Buy" button.
- On hover, the card should lift (
translateY+ deeper box-shadow). - The "Buy" button should change background color and scale up slightly on hover.
- Add a "wishlist" heart icon that transitions
colorandtransform: scale()when clicked (use JavaScript to toggle a class). - Ensure the image area has a subtle zoom effect on hover using
transform: scale(1.05)(clip withoverflow: hiddenon the container). - All transitions must use
transformoropacity— no animating layout properties. - Add a
prefers-reduced-motionmedia query that disables all transitions.
Bonus
- Add a tooltip that fades in on hover using the
opacity + visibilitytechnique. - Animate the underline of a nav bar using the transparent
border-bottomtechnique.
9. Assignment
Add transitions throughout an existing page.
- Take any HTML page with links, buttons, and form inputs.
- Add a hover transition to every button (background, shadow, transform).
- Add a focus transition to every input (border-color and box-shadow ring).
- Add hover transitions to all navigation links (color + underline).
- Implement an off-canvas sidebar that slides in from the left using a JS-toggled class and
transform: translateX. - Add a prefers-reduced-motion override block at the end of your CSS.
- Open DevTools → Rendering → Paint flashing. Verify that your transitions do not cause green flashes (which would indicate paint operations).
Deliverable: A single HTML file. Add a comment block at the top listing which transition techniques you used.
10. Interview Questions
- What is the difference between a CSS transition and a CSS animation?
A transition animates between two states (start and end) and is triggered by a property change. An animation (@keyframes) can have multiple intermediate steps and can run automatically without a trigger. - Why should you put the transition property on the base element rather than the :hover state?
If it is only on :hover, the transition plays going into hover but the property snaps back instantly when hover ends. On the base element, the transition plays in both directions. - Why are transform and opacity preferred over left/top/width for transitions?
transform and opacity are composited on the GPU, bypassing layout and paint stages. Changing width, height, top, left, etc. triggers layout recalculation, which is expensive and can cause jank. - What is the difference between ease-in and ease-out?
ease-in starts slowly and accelerates — best for elements leaving the screen. ease-out starts fast and decelerates — best for elements entering the screen, feeling natural as they "land". - How do you make a transition accessible for users who prefer reduced motion?
Use @media (prefers-reduced-motion: reduce) to disable or minimise transitions for users who have opted into reduced motion in their operating system settings. - Can you transition the display property?
Not directly — display switches between discrete values with no interpolation. Use opacity + visibility (or transform) instead, with a transition-delay on visibility to keep it timed with the opacity fade.
11. Additional Resources
- MDN — Using CSS transitions — comprehensive reference
- MDN — transition shorthand
- Google web.dev — Animations performance guide — why transform and opacity win
- easings.net — visual cubic-bezier easing function library
- cubic-bezier.com — interactive tool for designing custom easing curves
- Animate.style — CSS animation library to study real-world transition patterns