GODRICH

9 CSS Particle Backgrounds — One Line, Copy-Paste

A css particle background scatters small shapes across a stage and drifts each one along its own nth-child path with transform and opacity alone, no JavaScript

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine below are ordered by how their particles move, not by how flashy they look. 01, 04, 05, and 08 fall from above the stage. 02 and 03 hold their ground or slide sideways instead of falling. 06 climbs from the bottom up, and 07 wanders on a short, folding loop instead of committing to one direction. 09 closes the set as the odd one out — three wide bands sweep back and forth together instead of dozens of separate particles. Each code block below keeps only the rule that decides how many particles exist and how they're scattered; the complete file for all nine ships in the zip.

01Falling snow

24 small circles start above the frame, and nth-child hands each one a different left position, size, and fall duration before translateY and opacity carry it down. A single --i custom property multiplied by a negative offset staggers the delay, so all 24 loops are already mid-fall on first paint instead of starting stacked together. It fits a winter promo hero or the top of a year-end landing page.

nth-child(n)translateY--i 지연
.p {
  position: absolute; top: -8%; left: 0;
  width: 6px; height: 6px; border-radius: 50%;
  animation: fall 6s linear infinite;
  animation-delay: calc(var(--i) * -0.28s);
}
@for $i from 1 through 24 {
  .p:nth-child(#{$i}) { left: #{($i * 53) % 97}%; }
}
@keyframes fall {
  0%   { transform: translateY(0); opacity: 0; }
  100% { transform: translateY(340px); opacity: 0; }
}

02Twinkling stars

24 dots sit scattered across the stage, each blinking between 0.2 and 1 opacity on its own schedule rather than all 24 sharing one visible clock. A glowing box-shadow keeps every dot reading as a point of light instead of a flat disc. It works well behind a dark-mode landing hero or a night-themed event page.

opacity 트윙클nth-child 지연1.5~3s
.p {
  position: absolute; width: 5px; height: 5px; border-radius: 50%;
  box-shadow: 0 0 10px rgba(23, 20, 26, .55);
  animation: twinkle 1.8s ease-in-out infinite;
  animation-delay: calc(var(--i) * -0.31s);
}
@keyframes twinkle {
  0%, 100% { transform: scale(.7); opacity: .2; }
  50%      { transform: scale(1.3); opacity: 1; }
}

03SVG wave

The same wave path is drawn twice, side by side, inside a layer set to 200% width, and translateX slides that whole layer by exactly half its own width on a loop. Because the seam between the two copies is the same artwork meeting itself, the restart never shows. It suits a footer divider under a product section or the backdrop of a travel landing page.

SVG pathtranslateX 루프레이어 시차
.layer { position: absolute; left: 0; right: 0; bottom: 0; display: flex; width: 200%; }
.wave { width: 50%; flex: none; }
.layer--back  { animation: drift 6s linear infinite; }
.layer--front { animation: drift 3.9s linear infinite; }
@keyframes drift {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

04Falling rain

22 thin lines start tilted 12 degrees and fall diagonally in well under a second, with animation-delay staggering each line's start. The result reads as a bundle of streaks rather than one line repeating in sync. It fits a rainy-season promo background or the intro screen of a weather app.

translate 대각선nth-child 속도차0.4~0.7s
.p {
  position: absolute; top: -12%; width: 2px; height: 22px;
  transform: rotate(12deg);
  animation: rain .6s linear infinite;
  animation-delay: calc(var(--i) * -0.07s);
}
@keyframes rain {
  0%   { transform: translateY(0) translateX(0) rotate(12deg); opacity: .7; }
  100% { transform: translateY(380px) translateX(60px) rotate(12deg); opacity: .3; }
}

05Falling confetti

26 small rectangles fall while spinning, and a second @for loop hands alternating pieces an opposite rotation sign, so roughly half turn clockwise and half turn counterclockwise. Every third rectangle switches to a second color instead of the whole batch sharing one. It suits a payment-success screen, a signup celebration, or an event-winner page.

rotate + translateYnth-child 색상1.8~2.6s
.p { position: absolute; top: -10%; width: 7px; height: 11px; }
.p:nth-child(3n) { background: #2f6df6; }
@keyframes confetti {
  0%   { transform: translateY(0) rotate(0deg); opacity: 0; }
  100% { transform: translateY(360px) rotate(var(--spin, 360deg)); opacity: .85; }
}

06Rising bubbles

20 circles carry only a border and a faint fill, and translateY lifts each one from the bottom of the stage while scale grows then shrinks on the way up. The eased timing keeps the rise from reading as mechanical, closer to something actually pushing through liquid. It reads well behind a beverage or spa brand hero.

translateY 상승scale 맥동nth-child 크기
.p {
  position: absolute; bottom: -10%; width: 10px; height: 10px;
  border: 1.5px solid currentColor; border-radius: 50%;
  animation: rise 4s cubic-bezier(.2, .8, .2, 1) infinite;
}
@keyframes rise {
  0%   { transform: translateY(0) scale(.6); opacity: 0; }
  55%  { transform: translateY(-170px) scale(1.15); }
  100% { transform: translateY(-360px) scale(.8); opacity: 0; }
}

07Fireflies

16 soft dots glow through a blurred box-shadow and blink via opacity while a seven-stop keyframe changes their translate direction three separate times before folding back to the start. Because the path doubles back on itself instead of running in one line, each dot reads as wandering rather than falling. It fits a nature or camping brand background, or a mood-driven portfolio hero.

opacity 명멸translate 방향전환nth-child 궤적
.p {
  position: absolute; width: 4px; height: 4px; border-radius: 50%;
  box-shadow: 0 0 12px rgba(255, 247, 230, .8);
  animation: drift 3.5s ease-in-out infinite;
}
@keyframes drift {
  0%  { transform: translate(0, 0); opacity: 0; }
  35% { transform: translate(14px, -10px); opacity: .9; }
  55% { transform: translate(-8px, -20px); opacity: .3; }
  100% { transform: translate(0, 0); opacity: 0; }
}

08Falling sakura petals

18 small ellipses fall through a multi-stop keyframe that rotates and nudges each one sideways at three separate points on the way down, instead of dropping straight along one axis. That side-to-side nudge is what reads as wind rather than plain gravity. It suits a spring promo background or a wedding and beauty brand hero.

rotate + swayanimation-delaynth-child 낙하시간
.p { position: absolute; top: -10%; width: 10px; height: 6px; border-radius: 50%; }
@keyframes petal {
  0%   { transform: translate(0, 0) rotate(0deg); opacity: 0; }
  30%  { transform: translate(18px, 110px) rotate(120deg); }
  60%  { transform: translate(-14px, 240px) rotate(240deg); }
  100% { transform: translate(10px, 380px) rotate(360deg); opacity: .3; }
}

09Drifting fog

Three wide, blurred bands share one back-and-forth translateX loop running at the same speed, but animation-delay staggers when each band starts so all three overlap instead of moving as one flat sheet. Opacity breathes along the same cycle, which keeps the layer from ever settling into a static frame. It fits a mystery or horror content hero, or a mountain and outdoor brand background.

translateX 왕복opacity 숨쉬기animation-delay 위상
.p {
  position: absolute; left: -20%; width: 140%; height: 34%;
  filter: blur(6px);
  animation: driftx 2s linear infinite alternate;
}
.p:nth-child(2) { animation-delay: -0.66s; }
.p:nth-child(3) { animation-delay: -1.32s; }
@keyframes driftx {
  from { transform: translateX(-30%); opacity: .18; }
  to   { transform: translateX(30%);  opacity: .42; }
}

Where do these particles actually break?

Three separate bugs showed up while building this set, and each one hid in a different layer of the pipeline.

The first sits inside translate() itself. Its percentage values resolve against the moving element's own box, not the stage it crosses. Five of the nine — snow, rain, confetti, bubbles, and petals — were first written with something like translateY(370%), on the assumption that a particle would cross most of the stage. A 6px circle moving 370% of its own 6px height only travels 6 to 22 pixels, and the field barely twitched: item 01's cumulative-area reading sat at 0.15% before anyone noticed. Switching all five to a fixed pixel distance — translateY(340px) for the snow — pushed that same reading to 11.61%.

The second lives in item 09's blur. An early pass layered filter: blur(16px) on top of the opacity fade. The cumulative-area comparison between the first and last captured frame still read 94% — the band had, after all, ended up somewhere different — but the frame-to-frame movement count came back zero. Blur spreads a shape's edge across a wide, gradual transition instead of a hard line. The change between any two adjacent frames then fell under the pixel threshold this site's measurement script uses to call a frame "moved." Cutting the blur down and widening the band's own translateX range fixed it without touching the opacity curve.

The third is duration outrunning the capture window. That same fog band was first tuned to a 10-second loop, but this site's capture window runs only 2.0 seconds — a sliver that lands entirely inside ease-in-out's slow opening curve. So the reading came back as motionless. Matching the loop length to that 2.0-second window let the sweep register as motion again. Every one of the nine ships here with all three of those already fixed, inside an archive that only opens once fjv3dy5x goes in precisely as it's spelled on this page.

Accessibility

All nine lean on nothing newer than transform, opacity, @keyframes, :nth-child(), and CSS custom properties, which have been Baseline and widely available for years — no vendor prefix or fallback branch needed anywhere in the set. The one exception worth flagging is item 09's filter: blur(), which reaches back further still, to Chrome 53, Safari 9.1, and Firefox 35.

Under prefers-reduced-motion, none of the nine simply vanishes — each one keeps a faint, still trace of itself instead. Snow, rain, confetti, and petals stop moving and settle at a low fixed opacity near the same spot they'd otherwise be crossing. Stars and fireflies hold at a fixed glow instead of blinking. The wave layers and the fog bands stop outright, freezing the last painted frame, while bubbles hold near the bottom of their rise instead of resetting to invisible.

@media (prefers-reduced-motion: reduce) {
  .p { animation: none; opacity: .35; }
}

MDN's prefers-reduced-motion page covers the operating-system settings that flip this media query on. Browse other CSS-only patterns from the CSS category hub, or see what this site does and doesn't sell on the about page.

FAQ

Can I use just one of these as a single css particle background, not the whole set?

Yes. Every item's SCSS lives in its own .p block with a particle count and keyframes that never reference any other item. Lifting one span loop and its matching HTML out of the zip is enough on its own. Nothing here assumes the other eight are also on the page.

Does adding more particles slow the page down?

Not by itself. Every one of the nine only animates transform and opacity, properties a browser can composite on their own layer without recalculating the page's layout each frame. So the span count — from 3 in the fog to 26 in the confetti set — matters far less than which properties move. The exception is item 09's blur, which is why it's spread across three wide bands rather than scaled up to two dozen small ones. A blur's repaint cost tracks the area it covers, not the number of elements carrying it.

Why do the falling petals look blue instead of pink?

This site's demo palette has no pink in it, so item 08 borrows $subject-blue rather than adding a color used nowhere else on the site. Once the zip is open, swapping it back to pink is a one-line change to the $color variable and nothing else in the file needs to move.

Enter the archive password

The password is inside this article. You will find it as you read.