Transitions and Transforms
Introduction
Micro-interactions make interfaces feel responsive and polished, but only when motion is subtle, purposeful, and performant. This chapter introduces CSS transitions and transforms for common UI feedback patterns such as hover lift, button press states, and small reveal effects.
Prerequisites
- Basic selectors and pseudo-classes (
:hover,:focus-visible,:active) - Basic understanding of box model and layout
- Familiarity with accessibility basics for interaction feedback
Transition Basics
A transition animates property changes over time.
.button {
transition: background-color 0.2s ease;
}
.button:hover {
background-color: #1d4ed8;
}Code explanation:
- when hover changes
background-color, browser interpolates instead of jumping instantly - duration is
0.2s, timing curve isease
Transitions work only when a property value changes from one state to another.
Transition Properties
Longhand properties:
transition-propertytransition-durationtransition-timing-functiontransition-delay
Equivalent shorthand:
.card {
transition: transform 0.25s ease, box-shadow 0.25s ease;
}This is the most common production style: explicitly list animating properties.
Choosing Properties to Transition
Good candidates:
opacitytransformbackground-colorcolorbox-shadow(moderately)
Use caution with:
width,height,top,left,margin(can trigger heavier layout work)
Prefer transform/opacity for smoother UI feedback where possible.
Timing Functions
Common timing functions:
lineareaseease-inease-outease-in-outcubic-bezier(...)
Example:
.chip {
transition: transform 0.18s ease-out;
}ease-out often feels natural for hover entry/exit because motion settles smoothly.
Transition Delay
Use delay sparingly:
.tooltip {
transition: opacity 0.15s ease 0.05s;
}Small delays can reduce accidental flicker. Large delays can make UI feel unresponsive.
Transform Basics
transform visually changes an element without changing normal document flow.
Common transform functions:
translate(...)scale(...)rotate(...)skew(...)
Example:
.card:hover {
transform: translateY(-2px);
}Element appears to lift, while surrounding layout remains stable.
translate()
Moves element along axes:
.toast-enter {
transform: translateY(8px);
}Useful for subtle in/out motion and positional feedback.
scale()
Resizes visually:
.icon-button:hover {
transform: scale(1.04);
}Use small scale values to avoid jitter and layout overlap visuals.
rotate()
Rotates element:
.chevron-open {
transform: rotate(180deg);
}Very useful for accordion arrows and toggle indicators.
Combining Transform Functions
Transforms can be chained in one property:
.card:hover {
transform: translateY(-2px) scale(1.01);
}Order matters. Function sequence affects final visual result.
Transform Origin
Controls transform anchor point:
.menu {
transform-origin: top right;
}Useful for dropdowns, popovers, and rotating indicators.
Practical Pattern: Interactive Card
.card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.08);
}
.card:hover {
transform: translateY(-3px);
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.14);
}Why this works:
- lift motion is subtle
- elevation change reinforces hover state
- transition remains short and responsive
Practical Pattern: Button Press Feedback
.button {
transition: transform 0.08s ease, background-color 0.2s ease;
}
.button:active {
transform: translateY(1px);
}Small active press movement improves tactile feel.
Avoid transition: all
/* Avoid as default */
/* transition: all 0.3s ease; */Why avoid:
- animates unintended properties
- can introduce performance and debugging issues
- harder to reason about component behavior
Prefer explicit property lists.
Performance Notes
Generally smoother:
transformopacity
Potentially heavier when animated frequently:
- layout-affecting properties (
width,height,margin,left) - heavy blur/filter/shadow combinations on large elements
Keep animations short and meaningful.
Tip
Motion Should Communicate State
Animate to clarify interaction (hover, open, close, success), not as decoration on every element.
Accessibility: Reduced Motion
Respect user motion preferences:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}At minimum, reduce non-essential movement for users who request it.
Common Mistakes
| Mistake | Why it hurts | Better approach |
|---|---|---|
| Over-animating many properties | Distracting and slow | Animate only key properties |
| Large movement distances | Feels unnatural | Keep motion subtle (1-6px for micro-interactions) |
| Long durations for simple hover | UI feels laggy | Keep hover transitions short (120-250ms) |
| Using motion without focus/active states | Inconsistent interaction feedback | Pair hover with keyboard/touch state design |
| Ignoring reduced-motion preference | Accessibility issue | Add motion fallback with media query |
Debugging Tips
When transition feels wrong:
- verify property actually changes between states
- inspect computed transition values in DevTools
- shorten duration to test responsiveness
- ensure transform is not overwritten by another rule
- test with reduced-motion setting enabled
Use DevTools force state (:hover, :active, :focus-visible) for consistent testing.
Mini Exercise
- Create a card with hover lift (
translateY) and shadow transition - Add a button with active press feedback
- Add a rotating chevron icon for open/closed state
- Replace any
transition: allwith explicit property lists - Add reduced-motion fallback and test behavior
FAQ
Should I use transitions or keyframe animations for hover effects?
For simple state-to-state changes, transitions are usually simpler and more maintainable.
Why does my transition not run?
Most common reasons: property did not change, selector state never activated, or transition property list does not include the changing property.
Is animating box-shadow always bad?
Not always, but heavy shadow animation on many large elements can be expensive. Use subtly and test performance.
Why use transform for movement instead of top/left?
Transform-based movement often avoids layout recalculation and feels smoother for micro-interactions.
What is a good default transition duration?
For UI micro-interactions, around 150-250ms is a practical starting range.