9 CSS Confetti Animation & Sparkle Effects
A confetti animation is the small colored burst a screen fires when something succeeds — a finished signup, a completed order — and a css confetti animation
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Confetti burst on button
- 02 Confetti rain
- 03 Sparkle button
- 04 Sparkle text
- 05 Firework dots
- 06 Emoji pop
- 07 Pseudo-element-only confetti
- 08 Celebrate trigger via :has(:checked)
- 09 Sparkle cursor trail
These nine aren't ranked by how flashy they look — they follow how a screen actually earns each kind of celebration. First comes the payoff for an action someone just finished (01), then a piece that keeps celebrating on its own in the background (02), then two accents that draw the eye toward a button or a headline (03–04), then two showier bursts that launch from a single point (05–06), then two builds that skip extra markup or JavaScript entirely (07–08), and last, one that follows wherever the cursor goes (09).
01Confetti burst on button
Press a button right after a signup or checkout finishes, and twelve small pieces launch out from behind it in every direction, spinning as they travel, then fade away. An SCSS @for loop hands each of the twelve its own angle, travel distance, and start delay, so none of them move in lockstep. Pressing the button again replays the whole burst through 7 lines of plain JavaScript that just re-adds the animation class.
@for $i from 1 through 12 {
.cb-piece:nth-child(#{$i + 1}) {
background: nth($piece-colors, ($i - 1) % 4 + 1);
--angle: #{$i * 30deg};
--dist: #{64px + ($i % 3) * 22px};
animation-delay: #{($i % 4) * 40ms};
}
}
@keyframes cb-fly {
0% { transform: translate(-50%, -50%) rotate(var(--angle)) translateY(0) scale(.4); opacity: 0; }
15% { opacity: 1; }
100% { transform: translate(-50%, -50%) rotate(var(--angle)) translateY(calc(-1 * var(--dist))) scale(1); opacity: 0; }
}
02Confetti rain
Above a banner announcing a win, eighteen pieces of different sizes keep falling non-stop, each drifting down at its own pace so the scene reads as scattered rain rather than one falling column. Every piece gets its own animation-duration from an SCSS loop, and each one also starts from a different height that's deliberately out of step with its left-to-right position — lining the two up the same way made every piece leave at once along a single diagonal band instead of scattering.
$tops: -40px, 160px, -180px, 20px, 100px, -120px, 60px, -200px, 140px, -20px, 180px, -100px, 40px, -160px, 120px, -60px, 200px, -140px;
@for $i from 1 through 18 {
.cr-piece:nth-child(#{$i + 1}) {
left: percentage($i / 19);
top: nth($tops, $i);
background: nth($piece-colors, $i % 4 + 1);
animation-duration: #{1700ms + ($i % 5) * 140ms};
}
}
@keyframes cr-fall {
0% { transform: translateY(0) rotate(0deg); }
100% { transform: translateY(520px) rotate(300deg); }
}
03Sparkle button
Hover a premium or upgrade button and four small stars around its edge blink in and out one after another, each on its own animation-delay so they never flash together. Move the mouse away and they simply stop, with no lingering fade-out.
.sk-star {
position: absolute;
color: $color;
font-size: 16px;
line-height: 1;
opacity: 0;
transform: scale(.3);
pointer-events: none;
}
.sk-star--1 { top: -6px; left: 6%; }
.sk-star--2 { top: -14px; right: 10%; }
.sk-star--3 { bottom: -10px; left: 16%; }
.sk-star--4 { bottom: -4px; right: 4%; }
@keyframes sk-twinkle {
0%, 100% { opacity: 0; transform: scale(.3) rotate(0deg); }
50% { opacity: 1; transform: scale(1) rotate(90deg); }
}
04Sparkle text
Around a celebration headline, three small sparkles blink on their own timing, pulling attention to the words themselves rather than to a button nearby. Each one scales up and fades in on its own animation-delay, so the shimmer feels scattered instead of synchronized.
.st-star {
position: absolute;
color: $color;
font-size: 18px;
line-height: 1;
opacity: 0;
transform: scale(.2);
pointer-events: none;
}
.st-star--1 { top: -14px; left: -18px; color: $stage-orange; }
.st-star--2 { top: -20px; right: -14px; }
.st-star--3 { bottom: -16px; left: 40%; color: $stage-orange; }
@keyframes st-twinkle {
0%, 100% { opacity: 0; transform: scale(.2) rotate(0deg); }
50% { opacity: 1; transform: scale(1) rotate(70deg); }
}
.st-wrap.is-demo .st-star { animation: st-twinkle $duration $easing infinite; }
.st-star--2 { animation-delay: 260ms; }
.st-star--3 { animation-delay: 480ms; }
05Firework dots
One center point splits into eight, each dot carrying its own --angle custom property, and they shoot outward like a firework before shrinking away to nothing. The markup is just eight empty spans — every angle and color comes from an SCSS @for loop rather than hand-placed CSS.
@for $i from 1 through 8 {
.fw-dot:nth-child(#{$i}) {
background: nth($piece-colors, ($i - 1) % 4 + 1);
--angle: #{$i * 45deg};
}
}
@keyframes fw-burst {
0% { transform: rotate(var(--angle)) translateY(0) scale(1); opacity: 1; }
70% { opacity: 1; }
100% { transform: rotate(var(--angle)) translateY(-92px) scale(.3); opacity: 0; }
}
06Emoji pop
Tap a react button and three emoji hop up above it on staggered delays, each riding a keyframe that combines translateY and scale before fading out. Like item 01, pressing the button again replays the pop through the same short 7-line script that re-adds the animation class.
.ep-emoji--1 { left: 10%; }
.ep-emoji--2 { left: 44%; }
.ep-emoji--3 { right: 10%; }
@keyframes ep-pop {
0% { transform: translateY(0) scale(.4); opacity: 0; }
30% { opacity: 1; }
100% { transform: translateY(-64px) scale(1.1); opacity: 0; }
}
07Pseudo-element-only confetti
A finished-task card sheds confetti without a single extra element in its markup — only its ::before and ::after exist, and each one draws six dots at once through a stacked box-shadow list. The two pseudo-elements start at slightly different positions and delays so their dots don't fall as an obvious mirrored pair.
.pc-card::before,
.pc-card::after {
content: "";
position: absolute;
top: -18px;
left: 14px;
width: 6px;
height: 6px;
border-radius: $r-pill;
background: $color;
box-shadow:
22px -8px 0 -1px $stage-yellow,
44px 6px 0 0 $stage-orange,
66px -12px 0 -1px $subject-cream,
88px 4px 0 0 $color,
110px -6px 0 -1px $stage-yellow,
132px 8px 0 0 $subject-cream;
opacity: 0;
}
.pc-card::after { left: 4px; }
08Celebrate trigger via :has(:checked)
Check off a to-do item and eight confetti pieces fire around the checkbox with no JavaScript anywhere on the page — the parent label reads the checkbox's checked state directly with :has(:checked) and turns the animation on by itself. Uncheck it and the condition goes false again, so nothing replays until it's checked once more.
@keyframes hc-fly {
0% { transform: translate(-50%, -50%) rotate(var(--angle)) translateY(0) scale(.4); opacity: 0; }
20% { opacity: 1; }
100% { transform: translate(-50%, -50%) rotate(var(--angle)) translateY(-58px) scale(1); opacity: 0; }
}
.hc-wrap:has(.hc-check:checked) .hc-piece,
.hc-wrap.is-demo .hc-piece { animation: hc-fly $duration $easing infinite; }
09Sparkle cursor trail
Move the mouse across a promo banner and six sparkles trail the pointer, flickering on and off as they follow along. The banner stores the live cursor position in two custom properties, --mx and --my, each registered through @property with syntax: '<length>' so the browser can smoothly interpolate the pixel values between mouse positions instead of snapping between them.
@property --mx { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --my { syntax: '<length>'; inherits: true; initial-value: 0px; }
.sc-wrap {
position: relative;
width: min(280px, 84vw);
height: min(176px, 58vh);
border-radius: $r-card;
background: $subject-ink;
overflow: hidden;
--mx: 140px;
--my: 88px;
}
Where does a css confetti animation break?
The first real bug in this set showed up in item 02. Every piece originally got its own animation-delay and nothing else, and instead of scattered rain they all fell as one tight, synchronized diagonal band — pieces marching down together rather than spreading out. Giving each piece its own animation-duration looked like the fix, but that alone didn't work: the shared rule set $duration through an animation: shorthand, and a shorthand carries higher specificity than a lone animation-duration declared earlier on each piece, so it silently overwrote all eighteen custom durations back to one identical value. The real fix splits the shorthand apart — animation-name, animation-timing-function, and animation-iteration-count stay in one shared rule, while animation-duration is left alone on each numbered piece — paired with shuffling every piece's starting top offset out of sync with its left position, since matching the two in the same order rebuilt the very same diagonal band. Two of these nine also lean on fairly recent CSS: item 08's :has(:checked) trigger and item 09's @property registration. A browser that doesn't understand :has() just skips that whole rule block, so the checkbox still checks fine and nothing looks broken — the confetti simply never fires. One that ignores @property still animates --mx and --my, but without registering them as <length> first, the values can't be smoothly interpolated, so the sparkles jump between spots instead of drifting. The zip linked below needs a password to unlock it, and it's written into this very paragraph as an ordinary word rather than a code block: bjwrdptn. Copy it exactly as it appears here and the files open.
Variants worth trying
| Variant | Changed value | Feel |
|---|---|---|
| Bigger burst | Item 01's piece count and angle spacing changed from 12 pieces at 30deg apart to 20 pieces at 18deg apart | The burst reads as a full circle instead of a dozen spokes |
| Slower rain | Item 02's animation-duration range shifted from 1700ms–2400ms to 2600ms–3600ms |
The banner feels calmer, closer to snow than a downpour |
| Calmer twinkle | Item 03's $duration stretched from 1.1s to 2.2s |
The stars blink half as often, less distracting behind a CTA someone is trying to read |
Accessibility
All nine drop their motion under prefers-reduced-motion: reduce, but what's left behind depends on the item. For six of them — items 01, 02, 05, 06, 07, and the checked state in 08 — the celebration was decorative to begin with, so a still frame or an unanimated end state loses nothing a visitor needs: the button still shows it was pressed, the checkbox still shows it's checked, the banner still names the winner. Items 03, 04, and 09 are the same — their sparkle and cursor trail never carried information on their own, so switching them off just leaves the button, headline, or banner exactly as readable as before. Item 08's :has(:checked) state has nothing to do with the animation either way and keeps working with motion turned off entirely.
The same :has(:checked) technique behind item 08 gets a longer, checkbox-focused write-up in 9 CSS Checkbox Radio Animations, and item 06's click-triggered pop shares its short replay script with 9 CSS Heart Animations.
FAQ
Does a css confetti animation need JavaScript?
Only three of the nine do, and each uses about 7 lines. Items 01 and 06 replay their burst by re-adding an animation class on click, and item 09 tracks the real cursor position with a pointermove listener. The rest lean on nothing but selectors — :hover for a mouse, :checked for a box, :has() to read that checked box from outside it — never a script tag.
Will more confetti pieces slow the page down?
Twelve to eighteen pieces, the range items 01 and 02 use, animate only transform and opacity, so the browser skips layout recalculation and most devices handle it smoothly. Pushing the count into the hundreds adds enough composited layers that a low-power device can start dropping frames, so keeping whatever's visible at once under roughly twenty pieces is the safer ceiling.
Is the React version in the zip different from the plain CSS one?
The motion is identical. The plain CSS files keep three SCSS variables — $duration, $easing, $color — at the top of the file and drive everything through classes, while the React components take those same values as props and pass them through as inline CSS custom properties instead.