CSS Animations and Transitions: A Practical Guide
June 10, 2026 · 6 min read
CSS Animations and Transitions: A Practical Guide
Motion design is one of the biggest gaps between a UI that feels polished and one that feels static. CSS gives you two tools for adding movement: transitions (for state changes) and animations (for more complex, multi-step sequences). This guide covers both thoroughly, including performance best practices and accessibility considerations you can't afford to skip.
Use the CSS Animation Generator and CSS Transition Generator to build effects visually. And if you're building UI components without JavaScript, check out the Pure CSS UI Guide.
Transitions
A transition animates a CSS property from one value to another when a state change occurs — most commonly on :hover, :focus, or a class being added/removed via JavaScript.
Core Transition Properties
.button {
background-color: #3b82f6;
transition-property: background-color; /* which property to animate */
transition-duration: 200ms; /* how long */
transition-timing-function: ease-in-out; /* easing curve */
transition-delay: 0ms; /* wait before starting */
}
.button:hover {
background-color: #2563eb;
}
Shorthand syntax:
/* transition: property duration timing-function delay */
.button {
transition: background-color 200ms ease-in-out;
}
/* Multiple properties */
.button {
transition:
background-color 200ms ease,
transform 150ms ease-out,
box-shadow 200ms ease;
}
/* Transition everything (use sparingly — catches all changes) */
.button {
transition: all 200ms ease;
}
Timing Functions
The timing function controls the acceleration curve of the transition:
transition-timing-function: linear; /* constant speed */
transition-timing-function: ease; /* slow start and end (default) */
transition-timing-function: ease-in; /* slow start, fast end */
transition-timing-function: ease-out; /* fast start, slow end */
transition-timing-function: ease-in-out; /* slow both ends */
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1); /* custom curve */
transition-timing-function: steps(4); /* stepped, like frames */
ease-out is generally the best choice for UI interactions — it feels snappy because the motion starts fast and decelerates to the final position.
Common Use Cases: Hover Buttons
.btn {
display: inline-block;
padding: 10px 20px;
background: #3b82f6;
color: white;
border-radius: 6px;
border: none;
cursor: pointer;
transition: background-color 180ms ease-out, transform 120ms ease-out;
}
.btn:hover {
background-color: #1d4ed8;
transform: translateY(-2px);
}
.btn:active {
transform: translateY(0);
}
Common Use Cases: Nav Links
.nav-link {
position: relative;
color: #64748b;
text-decoration: none;
transition: color 150ms ease;
}
.nav-link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: #3b82f6;
transition: width 200ms ease-out;
}
.nav-link:hover {
color: #1e293b;
}
.nav-link:hover::after {
width: 100%;
}
Animations
CSS animations use @keyframes to define multi-step motion sequences. Unlike transitions, they can play automatically, loop, reverse, and run through multiple intermediate states.
@keyframes
Define animation steps with @keyframes. You can use percentages or the keywords from and to:
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
/* Multi-step with percentages */
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
Core Animation Properties
.element {
animation-name: fade-in;
animation-duration: 400ms;
animation-timing-function: ease-out;
animation-delay: 0ms;
animation-iteration-count: 1; /* or 'infinite' */
animation-direction: normal; /* normal, reverse, alternate, alternate-reverse */
animation-fill-mode: both; /* none, forwards, backwards, both */
animation-play-state: running; /* running or paused */
}
/* Shorthand: name duration timing delay count direction fill-mode */
.element {
animation: fade-in 400ms ease-out 0ms 1 normal both;
}
animation-fill-mode Explained
fill-mode is the most commonly misunderstood animation property:
none— element snaps back to original state when animation endsforwards— element keeps the final keyframe's styles after the animation endsbackwards— element applies the first keyframe's styles during the delay periodboth— applies bothforwardsandbackwardsbehavior
For most UI animations, animation-fill-mode: both is what you want.
Example: Fade-In
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: fade-in 350ms ease-out both;
}
/* Stagger cards */
.card:nth-child(2) { animation-delay: 50ms; }
.card:nth-child(3) { animation-delay: 100ms; }
.card:nth-child(4) { animation-delay: 150ms; }
Example: Slide-In
@keyframes slide-in-left {
from {
opacity: 0;
transform: translateX(-32px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.sidebar {
animation: slide-in-left 300ms ease-out both;
}
Example: Spin (Loading Indicator)
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid #e2e8f0;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 600ms linear infinite;
}
Example: Pulse (Attention/Loading)
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.skeleton {
background: #e2e8f0;
animation: pulse 1.5s ease-in-out infinite;
}
Performance: What to Animate and What to Avoid
This is the most important section if you care about smooth 60fps animations.
Use transform and opacity
These two properties are GPU-accelerated by most browsers. The browser can animate them on the compositor thread without triggering layout or paint:
/* GOOD — GPU-accelerated */
.element {
transition: transform 200ms ease, opacity 200ms ease;
}
.element:hover {
transform: translateX(10px);
opacity: 0.8;
}
Avoid Animating Layout Properties
These properties trigger layout recalculation on every frame, causing jank:
/* BAD — triggers layout on every frame */
.element {
transition: width 300ms ease, height 300ms ease, margin 300ms ease;
}
/* BAD — triggers paint on every frame */
.element {
transition: background-color 300ms ease; /* acceptable for short durations */
transition: box-shadow 300ms ease; /* better to fake with opacity */
}
Layout-triggering properties include: width, height, top, left, bottom, right, margin, padding, border-width, font-size.
Using will-change
Hint to the browser that an element will be animated, allowing it to promote the element to its own compositor layer in advance:
.animated-element {
will-change: transform, opacity;
}
Use will-change sparingly — it consumes GPU memory. Apply it only to elements you know will animate, and remove it after the animation if it's one-time.
Accessibility: prefers-reduced-motion
Some users have vestibular disorders, epilepsy, or simply prefer less motion. The prefers-reduced-motion media query lets you respect their system preferences:
/* Default: full animation */
.element {
animation: slide-in 400ms ease-out both;
transition: transform 200ms ease;
}
/* Reduced motion: disable or tone down */
@media (prefers-reduced-motion: reduce) {
.element {
animation: fade-in 100ms ease both; /* replace with simple fade */
transition: opacity 100ms ease; /* no movement */
}
}
A simple, broadly applicable approach:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
This effectively disables all animations and transitions for users who prefer it, while keeping any animation-fill-mode: forwards final states intact.
Practical Tips
Keep durations short for UI. Interactive elements (buttons, hovers) should transition in 100–200ms. Entering/exiting components (modals, drawers) work well at 250–350ms. Anything over 500ms starts to feel slow for UI.
Ease-out for entering, ease-in for exiting. Elements entering the screen feel natural decelerating into place. Elements leaving the screen feel natural accelerating out.
Don't animate multiple layout properties at once. If you need an element to expand, animate transform: scaleY() rather than height. The visual result is nearly identical but much smoother.
Pause on hover for infinite animations:
.spinner:hover {
animation-play-state: paused;
}
The CSS Animation Generator lets you build @keyframes sequences with a visual timeline and preview, while the CSS Transition Generator helps you tune easing curves interactively. For component patterns built entirely without JavaScript — including CSS-only accordions, dropdowns, and carousels — see the Pure CSS UI Guide.