CSS Animation Complete Guide: @keyframes, transition & Performance
CSS animations bring interfaces to life. From subtle hover effects to complex loading sequences, well-crafted animations improve user experience by providing visual feedback, guiding attention, and creating a sense of polish. CSS provides two complementary systems for animation: transition for simple state changes and @keyframes for complex multi-step animations.
This guide covers both systems in depth, along with performance optimization techniques that ensure your animations run at a smooth 60fps.
CSS Transitions: Animating State Changes
Transitions animate property changes between two states. They are triggered by state changes like :hover, :focus, class toggles, or JavaScript style changes.
Basic Transition Syntax
/* Transition a single property */
.button {
background-color: #2563eb;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #1d4ed8;
}
/* Transition multiple properties */
.card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
}
/* Shorthand: property duration timing-function delay */
.button {
transition: all 0.3s ease 0.1s;
}
/* Transition all animatable properties */
.link {
transition: all 0.2s ease-in-out;
}
Timing Functions
The timing function controls the acceleration curve of the transition:
transition-timing-function: ease; /* Default: slow start, fast middle, slow end */
transition-timing-function: linear; /* Constant speed */
transition-timing-function: ease-in; /* Slow start */
transition-timing-function: ease-out; /* Slow end */
transition-timing-function: ease-in-out; /* Slow start and end */
/* Custom cubic-bezier curve */
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55); /* Bounce effect */
The cubic-bezier() function lets you define any custom easing curve. Use tools like cubic-bezier.com to visualize and create curves.
@keyframes: Multi-Step Animations
While transitions handle two-state changes, @keyframes define animations with multiple steps, giving you full control over every frame.
Basic @keyframes Syntax
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Using percentage keyframes */
@keyframes slideInUp {
0% {
opacity: 0;
transform: translateY(30px);
}
50% {
opacity: 0.8;
}
100% {
opacity: 1;
transform: translateY(0);
}
}
Applying Animations
/* Individual properties */
.element {
animation-name: slideInUp;
animation-duration: 0.6s;
animation-timing-function: ease-out;
animation-delay: 0.2s;
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: forwards;
animation-play-state: running;
}
/* Shorthand (recommended) */
.element {
animation: slideInUp 0.6s ease-out 0.2s 1 normal forwards;
}
Animation Properties Explained
animation-name:The @keyframes name to useanimation-duration:How long one cycle takes (e.g.,0.6s)animation-timing-function:Easing curve (same as transition)animation-delay:Wait before starting (e.g.,0.2s)animation-iteration-count:Number of loops (1,3, orinfinite)animation-direction:normal,reverse,alternate,alternate-reverseanimation-fill-mode:What styles apply before/after (forwardskeeps end state)animation-play-state:runningorpaused
Practical Animation Patterns
Loading Spinner
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
Pulse Effect
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.notification {
animation: pulse 2s ease-in-out infinite;
}
Slide-In Navigation
@keyframes slideInLeft {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
.nav-open .nav-panel {
animation: slideInLeft 0.3s ease-out forwards;
}
Staggered Card Entrance
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
opacity: 0;
animation: fadeInUp 0.5s ease-out forwards;
}
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
.card:nth-child(4) { animation-delay: 0.4s; }
Typing Effect with Steps
@keyframes typing {
from { width: 0; }
to { width: 100%; }
}
@keyframes blink {
50% { border-color: transparent; }
}
.typewriter {
overflow: hidden;
white-space: nowrap;
border-right: 2px solid #333;
animation: typing 3s steps(20) forwards, blink 0.8s step-end infinite;
}
Performance Optimization
Performance is critical for animations. A janky 15fps animation is worse than no animation at all. Understanding how the browser renders animations is key to keeping them smooth.
The Golden Rule: Animate Only transform and opacity
The browser rendering pipeline has four stages: Style → Layout → Paint → Composite. Animating transform and opacity skips the Layout and Paint stages, running entirely on the GPU compositor thread. This is the single most important performance rule.
/* BAD — triggers layout recalculation */
.bad {
animation: moveBad 1s ease-in-out;
}
@keyframes moveBad {
from { left: 0; }
to { left: 100px; }
}
/* GOOD — runs on compositor thread only */
.good {
animation: moveGood 1s ease-in-out;
}
@keyframes moveGood {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}
Properties to avoid animating: width, height, top, left, margin, padding, border-width. These trigger layout recalculations.
will-change
The will-change property hints to the browser that an element will be animated, allowing it to prepare optimizations in advance.
.animated-element {
will-change: transform, opacity;
}
Use will-change sparingly. Do not apply it to many elements at once — it consumes memory. Remove it after the animation completes if possible.
Use transform Instead of Width/Height
/* BAD — triggers layout */
.accordion-content {
height: 0;
transition: height 0.3s ease;
}
.accordion-content.open {
height: 300px;
}
/* BETTER — use scaleY (approximation) */
.accordion-content {
transform: scaleY(0);
transform-origin: top;
transition: transform 0.3s ease;
}
.accordion-content.open {
transform: scaleY(1);
}
Accessibility: prefers-reduced-motion
Always respect the user's motion preferences. Some users experience motion sickness or have vestibular disorders.
/* Default: animations enabled */
@media (prefers-reduced-motion: no-preference) {
.animated {
animation: fadeInUp 0.5s ease-out;
}
}
/* Reduced motion: disable or simplify */
@media (prefers-reduced-motion: reduce) {
.animated {
animation: none;
opacity: 1;
transform: none;
}
.transition-element {
transition: none;
}
}
A simpler approach is to define animations by default and disable them for users who prefer reduced motion:
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
@media (prefers-reduced-motion: no-preference) {
* {
animation-duration: revert !important;
transition-duration: revert !important;
}
}
JavaScript Integration
CSS animations can be controlled with JavaScript for more dynamic behavior:
const element = document.querySelector('.element');
// Listen for animation events
element.addEventListener('animationstart', () => console.log('Started'));
element.addEventListener('animationend', () => console.log('Finished'));
element.addEventListener('animationiteration', () => console.log('Looped'));
// Pause and resume
element.style.animationPlayState = 'paused';
element.style.animationPlayState = 'running';
// Detect when transition ends
element.addEventListener('transitionend', (e) => {
if (e.propertyName === 'transform') {
console.log('Transform transition complete');
}
});
Frequently Asked Questions
Most numeric CSS properties can be animated: transform, opacity, color, width, height, margin, padding, font-size, box-shadow, and more. For best performance, stick to transform and opacity.
Transitions animate between two states triggered by a change (like hover). Animations run independently with multiple keyframes and can loop, pause, and reverse without a state change trigger.
Animate only transform and opacity (GPU-accelerated), use will-change sparingly, avoid animating layout properties like width/height/margin, and test on lower-end devices.
Yes, use animation-play-state: paused to pause and animation-play-state: running to resume. You can toggle this with JavaScript or CSS hover states.
Wrap animations in @media (prefers-reduced-motion: no-preference) or disable them with @media (prefers-reduced-motion: reduce) { animation: none; }. Always provide this accessibility consideration.