CSS animations let you create multi-step motion sequences that run on page load, loop infinitely, or trigger on demand — all without JavaScript. This guide covers @keyframes, every animation property, timing functions, and gives you 7 copy-paste recipes. Build them visually? Use the free animation builder →
CSS animations have two parts: a @keyframes rule that defines the animation steps, and the animation property that applies it to an element.
/* 1. Define the keyframes */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* 2. Apply to an element */
.card {
animation: fadeIn 0.5s ease-out forwards;
}
animation: name duration timing-function delay iteration-count direction fill-mode play-state;
/* Example */
animation: slideIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.2s 1 normal forwards running;
/* Multiple animations */
animation: fadeIn 0.5s ease-out, slideUp 0.6s ease-out 0.1s;
animation-name: fadeIn;
animation-duration: 0.5s;
animation-timing-function: ease-out; /* ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier() */
animation-delay: 0.2s; /* negative delays start mid-animation */
animation-iteration-count: 1; /* number | infinite */
animation-direction: normal; /* normal | reverse | alternate | alternate-reverse */
animation-fill-mode: forwards; /* none | forwards | backwards | both */
animation-play-state: running; /* running | paused */
A pulsing box using @keyframes with scale and opacity.
/* Fade in */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Slide up */
@keyframes slideUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
/* Scale in (pop) */
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
/* Bounce */
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); }
}
/* Spin */
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Pulse */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Shake (error feedback) */
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-8px); }
50% { transform: translateX(8px); }
75% { transform: translateX(-4px); }
}
The animation-timing-function controls the acceleration curve:
cubic-bezier(0.16, 1, 0.3, 1) (a snappy ease-out) feels more polished than the built-in ease-out. It's the curve used by most modern design systems.Some users are sensitive to motion. Always provide a reduced-motion fallback:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
<!-- Built-in animations -->
<div class="animate-spin">Loading</div>
<div class="animate-bounce">Scroll down</div>
<div class="animate-pulse">Loading skeleton</div>
<div class="animate-ping">Notification dot</div>
<!-- Custom in tailwind.config.js -->
animation: {
'fade-in': 'fadeIn 0.5s ease-out forwards',
}
keyframes: {
fadeIn: { from: { opacity: '0' }, to: { opacity: '1' } },
}
width, height, margin, or top/left is expensive.will-change: transform on elements you're about to animate — but remove it after the animation ends.Pick your timing function, set keyframes, preview the animation live — copy the full @keyframes CSS in one click.
Open the Animation Builder →Transitions animate between two states and require a trigger (like :hover or a class toggle). Animations use @keyframes to define multiple steps, can run on page load, loop infinitely, and do not need a trigger.
Set animation-iteration-count: infinite. For example: animation: spin 1s linear infinite.
It tells the element to keep the styles from the last keyframe after the animation ends, instead of snapping back to its original state.
Use the @media (prefers-reduced-motion: reduce) media query to set animation-duration to near zero and limit iteration count to 1. This respects the user's OS-level motion preference.