9 CSS Keyframe Animations, One Line, Copy-Paste
These nine CSS keyframe animation classes each wrap one keyframes block in a single class name, so adding that one class to any element runs the whole motion,
Auto-plays · click a tile to jump to its section · all nine in one zip
The nine below aren't ordered by name. They follow what a moving element is telling the page: 01 and 02 say it exists, 03 and 04 say it's working, 05 and 06 ask for a second look, and 07 through 09 mark a change that already happened. Skim the table below for the shape of each motion, then open any item's own section for the full class and its keyframes.
| Class | What moves | Duration | Loop |
|---|---|---|---|
.a-float |
translateY | 3s | infinite |
.a-bounce |
translateY | 1s | infinite |
.a-spin |
rotate | 1s | infinite |
.a-pulse |
scale, opacity | 2s | infinite |
.a-shake |
translateX | 500ms | once, on trigger |
.a-wobble |
translateX, rotate | 1s | infinite |
.a-flash |
opacity | 1s | infinite |
.a-heartbeat |
scale | 1.2s | infinite |
.a-jelly |
scaleX, scaleY | 600ms | once, on trigger |
01Float up and down
.a-float moves an element up 16px and back on an ease-in-out curve, so nothing about the motion reads as mechanical — it eases in and out the way a slow breath does. It suits an illustration or icon badge that needs to hold a corner of the screen without shouting for attention.
$duration: 3s;
$easing: ease-in-out;
.a-float { animation: float $duration $easing infinite; }
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-16px); }
}
02Bounce
.a-bounce runs the same translateY property as float but through five uneven keyframe stops instead of two, so it dips, overshoots, and settles the way something actually landing on a surface would. Scroll-hint arrows and "load more" icons use it to say a tap here does something, with no label needed.
$duration: 1s;
$easing: cubic-bezier(.28, .84, .42, 1);
.a-bounce { animation: bounce $duration $easing infinite; }
@keyframes bounce {
0%, 100% { transform: translateY(0); }
30% { transform: translateY(-4px); }
55% { transform: translateY(10px); }
75% { transform: translateY(-2px); }
90% { transform: translateY(4px); }
}
03Spin
.a-spin turns an element a full 360 degrees on a strict linear timing function, since any easing curve would make the rotation visibly speed up or slow down once per loop. It works equally well on a refresh icon or a sync icon, not only on a dedicated loading spinner.
$duration: 1s;
$easing: linear;
.a-spin { animation: spin $duration $easing infinite; }
@keyframes spin {
to { transform: rotate(360deg); }
}
04Pulse
.a-pulse eases scale between 1 and 1.06 while opacity dips slightly at the same midpoint, so a live-status dot or a CTA button gets a soft nudge of attention without ever repainting the page's layout. The two properties move together, so the opacity dip and the scale change never look out of sync.
$duration: 2s;
$easing: ease-out;
.a-pulse { animation: pulse $duration $easing infinite; }
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.06); opacity: .92; }
}
05Shake
.a-shake fires once on a trigger class rather than looping — six short left-right steps that finish in half a second and stop dead. It's built as a general "notice this" cue rather than a form-validation error, so it suits a new-feature badge just as well as a broken input.
$duration: 500ms;
$easing: cubic-bezier(.36, .07, .19, .97);
$amount: 8px;
.a-shake.is-shaking { animation: shake $duration $easing 1; }
@keyframes shake {
10%, 90% { transform: translateX($amount * -.5); }
20%, 80% { transform: translateX($amount); }
30%, 70% { transform: translateX($amount * -1); }
40%, 60% { transform: translateX($amount * .7); }
50% { transform: translateX($amount * -.7); }
}
06Wobble
.a-wobble layers translateX and rotate inside one keyframes block at staggered percentages, so it never settles into the clean back-and-forth of 05's shake. A draggable card or a list item mid-reorder leans on that extra unevenness to read as "move me" rather than "something's wrong."
$duration: 1s;
$easing: ease-in-out;
.a-wobble { animation: wobble $duration $easing infinite; }
@keyframes wobble {
0%, 100% { transform: translateX(0) rotate(0deg); }
15% { transform: translateX(-6px) rotate(-4deg); }
30% { transform: translateX(5px) rotate(3deg); }
45% { transform: translateX(-4px) rotate(-2deg); }
60% { transform: translateX(3px) rotate(2deg); }
75% { transform: translateX(-2px) rotate(-1deg); }
}
07Flash
.a-flash steps opacity from 1 down to roughly 0.15 and back, twice, instead of fading all the way to zero, so the element never fully disappears between blinks. An urgent-notice icon or a live-broadcast dot reads clearly against a light or a dark stage either way.
$duration: 1s;
$easing: ease-in-out;
.a-flash { animation: flash $duration $easing infinite; }
@keyframes flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: .15; }
}
08Heartbeat
.a-heartbeat splits its keyframes into two quick scale bumps followed by a longer rest, copying the uneven beat-beat-pause rhythm of an actual pulse instead of a smooth, evenly spaced loop. A like icon's idle state or a monitoring dashboard's status dot both benefit from that irregularity reading as "alive."
$duration: 1.2s;
$easing: ease-in-out;
.a-heartbeat { animation: heartbeat $duration $easing infinite; }
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
14% { transform: scale(1.24); }
28% { transform: scale(1); }
42% { transform: scale(1.18); }
70% { transform: scale(1); }
}
09Jelly wobble
.a-jelly moves scaleX and scaleY out of phase with each other, so an element squashes wide while it's short and tall while it's narrow before settling back into shape. It fires once, on release, which is exactly the timing an "added to cart" icon or a pressed button needs.
$duration: 600ms;
$easing: cubic-bezier(.68, -0.55, .27, 1.55);
.a-jelly.is-jellying { animation: jelly $duration $easing 1; }
@keyframes jelly {
0% { transform: scale(1, 1); }
20% { transform: scale(1.25, .75); }
40% { transform: scale(.75, 1.25); }
60% { transform: scale(1.15, .85); }
80% { transform: scale(.95, 1.05); }
100% { transform: scale(1, 1); }
}
What order do the animation shorthand values go in?
Writing .a-float's animation as animation: float 3s ease-in-out infinite; only works because each value lands in a fixed slot — swap two of them and a duration can get read as a delay instead. MDN's animation shorthand reference lists the eight slots in this order:
| Position | Sub-property | This post's example | Default if omitted |
|---|---|---|---|
| 1 | animation-name | float | none |
| 2 | animation-duration | 3s | 0s |
| 3 | animation-timing-function | ease-in-out | ease |
| 4 | animation-delay | (not set) | 0s |
| 5 | animation-iteration-count | infinite | 1 |
| 6 | animation-direction | (not set) | normal |
| 7 | animation-fill-mode | (not set) | none |
| 8 | animation-play-state | (not set) | running |
animation-fill-mode decides whether an element keeps a keyframe's styling before the animation starts or after it ends, and MDN's animation-fill-mode page documents all four keywords:
| Value | Holds the first frame before start | Holds the last frame after end |
|---|---|---|
| none | no | no |
| forwards | no | yes |
| backwards | yes | no |
| both | yes | yes |
05's shake and 09's jelly both run with a bare 1 for iteration-count and no fill-mode at all, and neither needs one: their 0% and 100% keyframes both land back on the resting transform, so the element returns to its starting look the instant playback finishes. A keyframes block whose last stop doesn't match the resting style needs forwards added, or the element snaps back to its pre-animation state the moment the animation ends.
Where it breaks — the trap
Stack two of these classes on one element — .a-float next to .a-spin, say — and only one of them actually moves. Each class declares the full animation shorthand, and the cascade lets whichever rule wins on order or specificity replace the other's value outright instead of merging with it. Combining two keyframes on the same element means writing animation-name: float, spin; and repeating animation-duration and animation-timing-function as matching comma-separated lists, one entry per name, in the same order. Even that isn't automatically safe: because float and spin both animate transform, the default animation-composition: replace still lets the second value in the list overwrite the first frame by frame, so only the rotation would show. Setting animation-composition: add — documented on MDN's animation-composition page — tells the browser to sum both transforms instead of replacing one with the other, so the element both floats and spins at once.
Item 07's flash measured the smallest footprint of the nine when this site's render check ran them: a 0.15% cumulative changed area, barely above the 0.08% floor the check requires before calling something "moving," since two opacity steps shift far fewer pixels than any transform in this set. The nine files already carry values tuned past that floor, unlocked with the password nft4q6u2, typed exactly as it reads on this line.
Accessibility
All nine drop out under prefers-reduced-motion: reduce, replacing animation: name duration easing infinite with animation: none per class rather than leaving the loop running at a faster or shorter length. 05's shake keeps its meaning without motion by switching to a background-color change on .is-shaking instead of the translateX steps, so a motion-sensitive viewer still gets told something happened. The other eight simply hold whatever static frame they were already resting on — a flat badge, an unblinking dot, and a button at scale(1) — since none of them carry information a static frame would lose. Which operating-system settings flip this media query on is listed on MDN's prefers-reduced-motion page.
Browse more of this line of demos on the CSS category hub, and see what's actually free to read here versus what's only in the zip on the about page.
FAQ
Can I use a CSS keyframe animation without Sass or a build step?
Yes — each class compiles down to plain CSS in the zip, so linking that stylesheet and adding the matching class name is all .a-float or .a-bounce need to run. The $duration and $easing variables only exist in the SCSS source for retuning the timing before compiling; the shipped CSS file already has fixed values baked in.
How do I run two of these on one element at once?
List every name in animation-name separated by commas, then repeat animation-duration and animation-timing-function with one comma-separated value per name in the same order. If both animations touch the same property, such as transform, add animation-composition: add so the values combine instead of the later one silently overwriting the earlier one — the trap section above walks through a .a-float plus .a-spin example.
What does the React version in the zip do differently from the class toggle?
The react/ folder's components take duration, easing, and color as props and pass them through as CSS custom properties instead of hard-coded SCSS variables. For the two that only play once, 05's shake and 09's jelly, the component flips a shaking or jellying flag off and straight back on with useState, then clears it again in onAnimationEnd, so clicking the button a second time replays the animation instead of doing nothing.