CSS offers two primary mechanisms for creating animations, each suited to different use cases. Understanding when to use each is the first step to creating smooth, performant web animations.
| Feature | Transitions | Keyframe Animations |
|---|---|---|
| Trigger | State change (:hover, class toggle) | Automatic or triggered |
| Complexity | Simple A → B | Multi-step sequences |
| Repetition | One-shot (per state change) | Looping or counted |
| Control | Limited | Full (direction, delay, iteration) |
| Best For | Hover effects, state changes | Load animations, complex sequences |
Transitions are the simplest form of CSS animation. They smoothly interpolate between property values when an element's state changes. The syntax is straightforward:
/* Shorthand syntax */
.button {
background: #4361ee;
color: #fff;
padding: 12px 24px;
border-radius: 8px;
border: none;
cursor: pointer;
/* property | duration | timing-function | delay */
transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(67, 97, 238, 0.4);
background: #3450d1;
}
/* Focus state for accessibility */
.button:focus-visible {
outline: 3px solid #ff9f1c;
outline-offset: 2px;
}
The timing function controls the acceleration curve of your animation. The built-in options serve different purposes:
| Function | Feel | Best For |
|---|---|---|
ease |
Starts fast, slows, fast finish | General purpose (default) |
ease-in |
Starts slow, accelerates | Elements leaving the screen |
ease-out |
Starts fast, decelerates | Elements entering the screen |
ease-in-out |
Slow start and end | State toggles, modals |
linear |
Constant speed | Progress bars, spinners |
For more complex animations, @keyframes gives you full control over every step of the animation sequence:
/* Fade-in and slide up */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Multi-step bounce animation */
@keyframes bounce {
0%, 100% {
transform: translateY(0);
animation-timing-function: ease-out;
}
50% {
transform: translateY(-20px);
animation-timing-function: ease-in;
}
}
/* Pulse animation */
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
}
/* Applying animations */
.hero-text {
animation: fadeInUp 0.8s ease-out both;
}
.notification-badge {
animation: pulse 2s ease-in-out infinite;
}
The animation shorthand combines several sub-properties:
/* Full shorthand: name | duration | timing | delay | iteration | direction | fill | play */
.card {
animation: fadeInUp 0.6s ease-out 0.2s 1 normal both running;
}
/* Breakdown of each property */
.card {
animation-name: fadeInUp;
animation-duration: 0.6s; /* How long */
animation-timing-function: ease-out; /* Speed curve */
animation-delay: 0.2s; /* Wait before starting */
animation-iteration-count: 1; /* How many times */
animation-direction: normal; /* normal, reverse, alternate */
animation-fill-mode: both; /* Styles before/after animation */
animation-play-state: running; /* running or paused */
}
🎯 Pro Tip: Use animation-fill-mode: both to apply the from styles before the animation starts (useful with delays) and the to styles after it ends. This prevents the "flash" of unstyled content before delayed animations begin.
The cubic-bezier() function gives you precise control over animation timing by defining a custom acceleration curve. It takes four values representing two control points:
/* Common cubic-bezier presets */
/* Material Design standard easing */
.material-standard { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
/* Material Design decelerate */
.material-decelerate { transition-timing-function: cubic-bezier(0, 0, 0.2, 1); }
/* Material Design accelerate */
.material-accelerate { transition-timing-function: cubic-bezier(0.4, 0, 1, 1); }
/* Snappy, elastic feel */
.snappy { transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55); }
/* Dramatic ease-out */
.dramatic { transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
The dramatic cubic-bezier (cubic-bezier(0.16, 1, 0.3, 1)) has become one of the most popular custom timing functions in modern web design. It creates a quick start with a long, graceful deceleration that feels premium and polished.
Smooth animation requires hitting 60 frames per second — that means the browser has approximately 16.67ms per frame to calculate and render every change. Understanding which CSS properties trigger expensive operations is critical.
| Category | Properties | Performance | Why |
|---|---|---|---|
| Composite | transform, opacity |
✅ Excellent | GPU-only, no layout or paint |
| Paint | color, background, box-shadow, border-radius |
⚠️ Moderate | Triggers repaint, may skip frames |
| Layout | width, height, margin, padding, top, left |
❌ Expensive | Triggers full layout recalculation |
⚠️ Golden Rule: Only animate transform and opacity for 60fps animations. These are the only properties that can be handled entirely by the GPU compositor without triggering layout or paint calculations.
/* ❌ BAD: Animating layout properties */
.card:hover {
left: 20px;
width: 300px;
margin-top: 10px;
}
/* ✅ GOOD: Using transform instead */
.card:hover {
transform: translateX(20px) scaleX(1.2);
}
/* ❌ BAD: Animating box-shadow (triggers paint every frame) */
.card:hover {
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
transition: box-shadow 0.3s;
}
/* ✅ GOOD: Animate opacity on a separate shadow element */
.card-shadow {
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: opacity 0.3s;
}
.card:hover .card-shadow {
opacity: 1;
}
/* ✅ BETTER: Use filter: drop-shadow (composited in some browsers) */
.card:hover {
filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.3));
transition: filter 0.3s;
}
Forcing an element onto its own GPU layer can dramatically improve animation performance:
/* Promote to GPU layer */
.animated-element {
will-change: transform;
/* Or use: transform: translateZ(0); */
/* Or use: backface-visibility: hidden; */
}
💡 Important: Don't overuse will-change. Each promoted layer consumes GPU memory. Only use it on elements that are actively animating, and remove it after the animation completes.
/* Stagger entrance of list items */
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.list-item {
animation: slideIn 0.4s ease-out both;
}
/* Use CSS custom property for stagger delay */
.list-item:nth-child(1) { animation-delay: 0ms; }
.list-item:nth-child(2) { animation-delay: 60ms; }
.list-item:nth-child(3) { animation-delay: 120ms; }
.list-item:nth-child(4) { animation-delay: 180ms; }
.list-item:nth-child(5) { animation-delay: 240ms; }
/* Or with a single rule using calc */
.list-item {
animation-delay: calc(var(--index) * 60ms);
}
/* Smooth page content transitions */
@keyframes pageEnter {
from {
opacity: 0;
transform: translateY(10px);
}
}
.page-content {
animation: pageEnter 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
}
/* Skeleton loading animation */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(
90deg,
#e0e0e0 25%,
#f0f0f0 50%,
#e0e0e0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
/* Button press effect */
.button {
transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.button:active {
transform: scale(0.97);
}
/* Checkbox animation */
@keyframes checkmark {
0% {
stroke-dashoffset: 24;
}
100% {
stroke-dashoffset: 0;
}
}
.checkbox-icon {
stroke-dasharray: 24;
stroke-dashoffset: 24;
transition: stroke-dashoffset 0.3s ease;
}
.checkbox-checked .checkbox-icon {
stroke-dashoffset: 0;
}
/* Toggle switch */
.toggle-track {
transition: background 0.3s ease;
}
.toggle-thumb {
transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.toggle-active .toggle-thumb {
transform: translateX(24px);
}
In 2026, scroll-driven animations have become a powerful native CSS feature. Instead of JavaScript scroll listeners, you can tie animations directly to scroll position:
/* Scroll-driven fade-in (2026 CSS) */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.scroll-reveal {
animation: fadeIn linear both;
animation-timeline: view();
animation-range: entry 0% entry 50%;
}
/* Parallax scrolling */
@keyframes parallax {
from { transform: translateY(0); }
to { transform: translateY(-100px); }
}
.parallax-bg {
animation: parallax linear both;
animation-timeline: scroll();
animation-range: 0px 500px;
}
Scroll-driven animations eliminate the need for Intersection Observer and scroll event listeners for many common patterns, resulting in smoother performance and less JavaScript.
Accessibility requires that animations respect user preferences. Many users experience motion sickness, vestibular disorders, or simply find animations distracting. The prefers-reduced-motion media query lets you provide alternative experiences:
/* Default: full animations */
.card {
animation: fadeInUp 0.6s ease-out both;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
/* Reduced motion: instant or minimal animation */
@media (prefers-reduced-motion: reduce) {
.card {
animation: none;
opacity: 1;
transform: none;
/* Keep transitions but make them instant */
transition-duration: 0.01ms !important;
}
/* Or: keep transitions but remove motion */
.card:hover {
transform: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* Remove auto-playing animations */
.auto-animate {
animation: none !important;
}
}
🎯 Best Practice: Don't just disable all animations for reduced-motion users. Remove decorative motion but keep essential state-change feedback. A button hover should still indicate interactivity — just without the movement.
| Animation Type | Recommended Duration |
|---|---|
| Micro-interactions (hover, press) | 100–200ms |
| State transitions (toggle, expand) | 200–400ms |
| Entrance animations (fade in, slide) | 300–600ms |
| Page transitions | 300–500ms |
| Loading indicators | 800–1500ms (loop) |
transform and opacity for smooth 60fpsease-out for elements entering the viewportease-in for elements leaving the viewportcubic-bezier(0.16, 1, 0.3, 1) for premium-feeling decelerationprefers-reduced-motion alternativeswill-change sparingly and only on actively animating elementswidth, height, top, or left — use transform insteadanimation-delay without animation-fill-mode: bothwill-change after animations completeCSS animations have evolved from simple novelties into essential design tools. When used thoughtfully, they guide user attention, provide feedback, create spatial relationships, and make interfaces feel alive and responsive. The key is restraint — the best animations are the ones users notice but don't consciously think about. They feel natural, not flashy. With the performance principles and patterns from this guide, you're equipped to create animations that are both beautiful and blazingly fast.
Design, preview, and export production-ready CSS animation code with our interactive generator. Choose from presets or build custom keyframe animations.
→ Try CSS Animation Generator