Transitions and Animations
Motion communicates state changes and guides attention. But poor animation degrades experience: sluggish interactions feel unresponsive, excessive motion causes fatigue. This lesson covers when to use animation, how to make it feel natural, and performance constraints that keep interfaces fast.
When Not to Animate
Before learning how to animate, understand when animation is wrong.
The frequency principle: elements used 100+ times per day should not animate. Every button click, every menu toggle, every tab switch compounds. A 200ms animation feels fine once. After 100 interactions, those 20 seconds accumulate into noticeable delay. High-frequency actions must be instant.
Examples of high-frequency interactions:
- Clicking buttons in a form
- Switching tabs
- Opening/closing dropdowns
- Selecting items in a list
- Keyboard navigation
These should have no delay. State changes should be immediate.
Use animation for:
- Page transitions (low frequency)
- Modal dialogs (infrequent, high attention cost)
- Success/error feedback (infrequent, needs attention)
- First-time experiences (happens once)
- Decorative elements that don't block interaction
Transitions
Transitions animate property changes over time:
.button {
background: blue;
transition: background 0.2s;
}
.button:hover {
background: darkblue;
}When background changes, it animates over 0.2 seconds instead of switching instantly.
The transition Shorthand
The transition property combines four sub-properties:
.element {
transition: property duration timing-function delay;
}Examples:
/* Single property */
.button {
transition: background 0.2s ease-out;
}
/* Multiple properties */
.button {
transition:
background 0.2s ease-out,
transform 0.2s ease-out;
}
/* All properties (avoid this) */
.button {
transition: all 0.2s;
}Transition Properties
You can transition most CSS properties, but not all transitions perform well:
Fast (GPU-accelerated):
transform(translate, rotate, scale)opacity
Slow (causes repaints or reflows):
width,heighttop,left,margin,paddingbackground(acceptable for small elements)color(acceptable for text)
Use transform instead of position properties:
/* Bad - animates layout */
.panel {
left: -300px;
transition: left 0.3s;
}
.panel.open {
left: 0;
}
/* Good - animates transform */
.panel {
transform: translateX(-100%);
transition: transform 0.3s;
}
.panel.open {
transform: translateX(0);
}Both achieve the same visual result, but transform runs on the GPU and doesn't trigger layout recalculation.
Timing Functions
Timing functions (easing) control how property values change over time. The wrong easing makes animation feel mechanical or sluggish.
The Easing Blueprint
Use these easing functions for different interaction types:
ease-out: Elements entering the viewport or appearing
.modal {
opacity: 0;
transform: scale(0.9);
transition:
opacity 0.25s ease-out,
transform 0.25s ease-out;
}
.modal.open {
opacity: 1;
transform: scale(1);
}Starts fast, decelerates at the end. This feels natural for elements coming to rest.
ease-in-out: Elements moving from one position to another
.slider-item {
transition: transform 0.3s ease-in-out;
}Accelerates at the start, decelerates at the end. Use for movement where the element stays visible throughout.
ease: Hover and interaction feedback
.button {
transition: background 0.15s ease;
}The default easing. Starts fast, slight deceleration. Good for subtle interactive feedback.
Custom Timing with cubic-bezier()
Create custom easing curves with cubic-bezier():
.element {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}The four numbers define two control points on a cubic curve. Most designers use tools to generate these values rather than writing them manually. Stick to standard easing keywords unless you need precise custom motion.
Steps
The steps() function creates frame-by-frame animation:
.sprite {
background: url('sprite-sheet.png');
animation: sprite 1s steps(10) infinite;
}
@keyframes sprite {
to {
background-position: -1000px 0;
}
}This is useful for sprite sheet animations where you want to jump between discrete frames rather than smoothly interpolate.
Duration
Duration determines how long an animation takes. Too fast is jarring, too slow is tedious.
Duration guidelines:
- Micro interactions (100–150ms): Hover states, focus rings, checkbox toggles
- Standard transitions (150–250ms): Buttons, dropdowns, tab switches
- Modals and overlays (200–300ms): Dialogs, sheets, popovers
- Page transitions (300–400ms): Route changes, full-screen transitions
/* Hover feedback - fast */
.button:hover {
background: darkblue;
transition: background 0.15s;
}
/* Dropdown menu - standard */
.menu {
transition: opacity 0.2s;
}
/* Modal dialog - deliberate */
.modal {
transition: transform 0.25s;
}Longer animations work for infrequent, high-impact moments. Shorter animations work for frequent interactions. Remember the frequency principle: 100ms × 100 clicks = 10 seconds of accumulated delay.
Button Press Feel
Make buttons feel responsive with scale on press:
.button {
transition: transform 0.1s ease;
}
.button:active {
transform: scale(0.97);
}The slight squish on press provides tactile feedback. This should be fast (100ms) and subtle (0.95–0.98 scale). Don't animate :active state itself—apply the transform instantly, animate the transition back to normal.
Keyframe Animations
Keyframe animations define multi-step sequences:
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.element {
animation: fadeInUp 0.3s ease-out;
}Keyframes create named animations that you apply with the animation property. You can define intermediate steps:
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.notification {
animation: pulse 0.6s ease-in-out;
}Animation Properties
The animation property has multiple sub-properties:
.element {
animation-name: fadeIn;
animation-duration: 0.3s;
animation-timing-function: ease-out;
animation-delay: 0.1s;
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: forwards;
}Shorthand:
.element {
animation: fadeIn 0.3s ease-out 0.1s 1 normal forwards;
}Common properties:
animation-iteration-count: infinite- Loop foreveranimation-fill-mode: forwards- Keep final state after animation endsanimation-fill-mode: backwards- Apply first keyframe before animation startsanimation-direction: alternate- Reverse direction each iteration
Pairing Modals and Overlays
When animating modals, animate the overlay and modal with the same duration and easing:
.overlay {
transition: opacity 0.25s ease-out;
}
.modal {
transition:
opacity 0.25s ease-out,
transform 0.25s ease-out;
}Mismatched timing looks broken. If the overlay fades in 200ms but the modal takes 300ms, the experience feels uncoordinated. Paired elements should move together.
Performance: Transform and Opacity Only
For smooth 60fps animation, only animate transform and opacity:
/* Good - GPU accelerated */
.element {
transition:
transform 0.3s,
opacity 0.3s;
}
/* Bad - causes layout recalculation */
.element {
transition:
width 0.3s,
height 0.3s,
top 0.3s;
}Animating width, height, or position properties forces the browser to recalculate layout for every frame. This causes jank on slower devices. Use transform: scale() instead of animating dimensions, transform: translate() instead of animating position.
The will-change Property
will-change hints to the browser that a property will animate:
.modal {
will-change: transform, opacity;
}This tells the browser to optimize for animating those properties. But don't overuse it. will-change consumes memory. Apply it only to elements that will actually animate soon, and remove it when animation is done:
.modal {
/* Don't add will-change here */
}
.modal.opening {
will-change: transform, opacity;
}
.modal.open {
will-change: auto; /* Remove optimization */
}For most cases, you don't need will-change. The browser optimizes transform and opacity automatically.
Reduced Motion
Some users experience nausea or disorientation from animation. Respect their preferences:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}This disables all animations for users who've enabled reduced motion in their OS settings. You can be more selective:
.modal {
transition: transform 0.25s ease-out;
}
@media (prefers-reduced-motion: reduce) {
.modal {
transition: none;
}
}Every animation you create must respect prefers-reduced-motion. This is an accessibility requirement, not optional. Users with vestibular disorders, ADHD, or motion sensitivity need this.
Scroll-Driven Animations
CSS can drive animations based on scroll position:
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.reveal {
animation: fadeIn linear;
animation-timeline: view();
}animation-timeline: view() makes the animation progress as the element scrolls into view. This replaces JavaScript scroll listeners for common reveal effects.
Control the scroll range:
.reveal {
animation: fadeIn linear;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}The animation runs as the element enters the viewport (entry 0% to entry 100%). You can specify ranges like cover 0% cover 50% (first half of element covering viewport) or exit 0% exit 100% (element leaving viewport).
Check Your Understanding
Which timing function should you use for elements entering the viewport?
Why should you avoid animating width and height?
According to the frequency principle, when should you NOT use animation?
Practice
Summary
- Frequency principle: Don't animate elements used 100+ times per day—accumulated delay degrades experience
- Use animation for: Page transitions, modals, feedback, first-time experiences (low-frequency moments)
- Transition shorthand:
transition: property duration timing-function delay - Never transition all: Always specify exact properties to avoid performance issues
- Performance: Only animate
transformandopacityfor GPU acceleration and smooth 60fps - Easing blueprint: Use
ease-outfor enter/exit,ease-in-outfor movement,easefor hover - Never use ease-in: Starts slow, makes interactions feel sluggish and unresponsive
- Duration guidelines: Micro 100–150ms, standard 150–250ms, modals 200–300ms, pages 300–400ms
- Button press feel: Use
transform: scale(0.97)on:activefor tactile feedback - Paired elements: Animate modal and overlay with matching duration and easing
- Keyframe animations: Use
@keyframesfor multi-step sequences withanimationproperty - will-change: Only use when needed, remove after animation completes
- Reduced motion: Every animation must respect
prefers-reduced-motionfor accessibility - Scroll animations: Use
animation-timeline: view()for scroll-driven effects