CSS Animation Keyframes: Copy-Paste Examples
Most "CSS animation keyframes examples" you can copy and paste fall into a handful of effects: a loading spinner, a fade-in, a bounce, a pulse, a shake, and a staggered bouncing-dots loader. Below is each one as drop-in CSS you can lift straight into a stylesheet. After the snippets, there are the two things copy-paste posts almost always skip: which properties are safe to animate for smooth performance, and the accessibility block you should include. When you want to tune timing or swap the easing curve, the CSS animation generator and the CSS easing visualizer turn this guesswork into a live preview.
Spinner (the border trick)
The classic loading spinner is a circle with a transparent border on three sides and one visible edge, rotated forever. No SVG, no image, one element.
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
The linear timing function keeps rotation at a constant speed, which is what reads as a "spinner" rather than a pulse. A single to keyframe is all you need because the browser animates from the element's current value (0deg) to the one you declare.
Fade-in
Fade-in animates opacity from 0 to 1. To also slide content up slightly, combine it with a translateY in the same keyframes. Both properties are GPU-friendly, which matters for the rule later in this article.
.fade-in {
animation: fade-in 0.5s ease-out both;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
The both fill mode applies the from state before the animation starts and holds the to state after it ends, so the element does not flash to full opacity before animating.
Bounce and pulse
Bounce moves an element up and back down with a snappy ease, while pulse scales it gently for an "alive" or attention effect. Both loop with infinite.
.bounce {
animation: bounce 1s ease infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); }
}
.pulse {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.08); opacity: 0.8; }
}
Shake (for invalid input)
Shake is a short horizontal jitter, typically triggered when a form field is invalid. Keep it brief and do not loop it, otherwise it becomes a distraction rather than feedback.
.shake {
animation: shake 0.4s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
20%, 60% { transform: translateX(-8px); }
40%, 80% { transform: translateX(8px); }
}
Bouncing-dots loader (staggered animation-delay)
Three dots that bounce in sequence is one of the most-requested loaders. The trick is one keyframe rule shared by all three dots, with a negative animation-delay on each to offset the phase. Negative delays start the animation as if it had already been running, so the dots are staggered from the very first frame instead of waiting.
.dots {
display: inline-flex;
gap: 6px;
}
.dots span {
width: 10px;
height: 10px;
border-radius: 50%;
background: #3b82f6;
animation: dot-bounce 1.2s ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay: -0.96s; }
.dots span:nth-child(3) { animation-delay: -0.72s; }
@keyframes dot-bounce {
0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
40% { transform: translateY(-10px); opacity: 1; }
}
The markup is just <div class="dots"><span></span><span></span><span></span></div>. Adjust the gap between delays to make the wave faster or slower.
Performance: animate transform and opacity only
This is the rule that separates a smooth animation from a janky one. Per MDN and Google's web.dev guidance, transform and opacity can be handled by the browser's compositor without recalculating layout or repainting the page, which lets them run on the GPU and stay near 60fps. Animating width, height, top, left, or margin forces the browser to re-run layout on every frame, which is the most expensive work it can do and the usual cause of stutter.
- Move things with
transform: translate(), nottop/left/margin. - Resize things with
transform: scale(), notwidth/height. - Fade things with
opacity, nevervisibilitytransitions or color swaps for a fade.
Every snippet above already follows this rule, which is why they are smooth on mobile. If you must hint the browser to promote an element to its own layer, will-change: transform can help, but use it sparingly because every promoted layer costs memory.
Accessibility: include a prefers-reduced-motion block
Motion can trigger nausea, dizziness, and migraines in people with vestibular disorders. Operating systems expose a "reduce motion" setting, and the browser surfaces it through the prefers-reduced-motion media query. Honoring it is a well-established best practice: WCAG 2.1 Success Criterion 2.3.3 (Animation from Interactions, Level AAA) calls for letting users disable interaction-triggered motion, and SC 2.2.2 (Pause, Stop, Hide, Level A) covers content that moves automatically. Respecting the media query is the simplest way to satisfy the spirit of both. Add this block once and it tones down runaway motion across your whole site:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
This near-zero duration approach keeps the end state intact (a faded-in element still ends up visible) while removing the perceived movement. For a spinner that genuinely needs to convey "loading," consider swapping it for a static or text indicator under reduced motion rather than freezing a half-spun graphic. If you also work with layout animations, the same compositor-first thinking applies to your overall structure, covered in the 2026 CSS layout techniques guide.
Putting it together
Each animation is three parts: the animation shorthand on the element (name, duration, easing, iteration, fill), the @keyframes rule that defines the motion, and the reduced-motion override that protects users who opt out. Keep durations short (0.3s to 1.2s for most UI), prefer transform and opacity, and let the curve do the personality. To dial in the exact timing and easing without trial-and-error refreshes, paste a snippet into the animation generator and pick a curve in the easing visualizer until it feels right.
Frequently Asked Questions
Animate transform and opacity. Per MDN and web.dev, the browser can composite these on the GPU without recalculating layout or repainting, so they stay near 60fps. Avoid animating width, height, top, left, or margin, which force a layout recalculation on every frame and cause visible jank, especially on mobile devices.
Use one element with a circular border-radius, a thin border with a transparent base color, and a single visible border-top-color. Then animate transform: rotate(360deg) with a linear timing function set to infinite. The constant linear speed is what reads as a spinner rather than a pulse, and no SVG or image is needed.
A negative animation-delay starts the animation as if it had already been running for that amount of time. Applying different negative delays to each dot offsets their phase so they are staggered from the very first frame, instead of all starting together and only desynchronizing after a full cycle. It creates the wave effect instantly.
It is a strongly recommended accessibility best practice rather than a strict Level AA legal requirement. Motion can trigger dizziness, nausea, and migraines for people with vestibular disorders. WCAG 2.1 Success Criterion 2.3.3 (Level AAA) addresses interaction-triggered motion. Adding a @media (prefers-reduced-motion: reduce) block that sets durations to near zero gives users who opt out a still, usable interface.
The both fill mode applies the from keyframe's styles before the animation starts and keeps the to keyframe's styles after it finishes. For a fade-in, this prevents the element from flashing at full opacity before the animation begins and ensures it stays visible at the end rather than snapping back to its un-animated default state.