Guides/ CSS
ac_unit

CSS Snowfall Effect Tutorial: Pure-CSS Snow & Seasonal UI

Seasonal effects like falling snow are the easiest way to make a holiday website feel festive — and the easiest way to tank your page's performance if done wrong. This guide covers how to build a performant pure-CSS snowfall, plus when to stop and use canvas instead.

April 2026 · 6 min read

The Minimum Viable Snow Particle

A single "snowflake" is a small white circle that falls from top to bottom of the viewport while drifting horizontally. The base building block:

.snowflake {
  position: fixed;
  top: -10px;
  width: 8px;
  height: 8px;
  background: white;
  border-radius: 50%;
  opacity: 0.8;
  animation: fall 10s linear infinite;
  pointer-events: none;
}

@keyframes fall {
  0% {
    transform: translateY(0) translateX(0);
    opacity: 0.8;
  }
  100% {
    transform: translateY(110vh) translateX(20px);
    opacity: 0;
  }
}

The 110vh ensures the flake exits below the viewport. Fading opacity at the end prevents flakes from abruptly disappearing. position: fixed keeps them floating over content regardless of scroll.

Randomizing Multiple Flakes

The trick to convincing snow is variation — different starting positions, sizes, speeds, and horizontal drift. Pure CSS can do this with per-element custom properties:

<div class="snow">
  <span class="snowflake" style="--left:10%; --duration:8s; --delay:-2s; --size:6px; --drift:40px;"></span>
  <span class="snowflake" style="--left:30%; --duration:12s; --delay:-5s; --size:10px; --drift:-30px;"></span>
  <span class="snowflake" style="--left:50%; --duration:15s; --delay:-8s; --size:8px; --drift:50px;"></span>
  <span class="snowflake" style="--left:70%; --duration:10s; --delay:-3s; --size:12px; --drift:-20px;"></span>
  <span class="snowflake" style="--left:90%; --duration:13s; --delay:-6s; --size:7px; --drift:30px;"></span>
</div>

<style>
.snowflake {
  position: fixed;
  top: -20px;
  left: var(--left);
  width: var(--size);
  height: var(--size);
  background: white;
  border-radius: 50%;
  opacity: 0.8;
  animation: fall var(--duration) linear infinite;
  animation-delay: var(--delay);
  pointer-events: none;
}

@keyframes fall {
  0%   { transform: translateY(0) translateX(0); opacity: 0.8; }
  100% { transform: translateY(110vh) translateX(var(--drift)); opacity: 0; }
}
</style>

Negative animation-delay values start each flake mid-animation, so the effect is active immediately on page load rather than waiting 10 seconds for the first flake to reach the bottom.

Adding Natural Wobble

Real snow drifts in a curving path, not a straight diagonal. Add wobble with intermediate keyframe steps:

@keyframes fall {
  0%   { transform: translateY(0) translateX(0); }
  25%  { transform: translateY(25vh) translateX(20px); }
  50%  { transform: translateY(55vh) translateX(-15px); }
  75%  { transform: translateY(80vh) translateX(10px); }
  100% { transform: translateY(110vh) translateX(-5px); opacity: 0; }
}

Using ease-in-out instead of linear creates gentle acceleration at peaks. The multiple X offsets create a zigzag that reads as wind drift.

Size Variation for Depth

Snow that all falls at the same speed and size looks fake. Simulate depth by:

  • Larger flakes fall faster (closer to viewer)
  • Smaller flakes fall slower and are more transparent (further away)
  • Blur distant flakes slightlyfilter: blur(1px) on small size flakes reads as depth of field
  • Mix sizes 4px–14px across your flake elements for visual variety

Performance Budget

Every snowflake animation consumes GPU resources. The practical limits:

  • 30-50 flakes — fine on any device. Visually sufficient for most sites.
  • 50-150 flakes — OK on desktop, may stutter on older mobile devices.
  • 150+ flakes — CSS starts struggling. Consider Canvas API instead.
  • Always use transform for movement — never top or left, which trigger layout.
  • Add will-change: transform to flakes, but remove after the season ends.
  • Respect prefers-reduced-motion — users who've disabled motion won't enjoy snow.
@media (prefers-reduced-motion: reduce) {
  .snowflake {
    animation: none;
    display: none;
  }
}

Alternative Styles

Snowflake Emoji / Unicode

For stylized flakes instead of plain circles, use Unicode characters inside the span:

<span class="snowflake">❄</span> /* classic snowflake */
<span class="snowflake">❆</span> /* heavier version */
<span class="snowflake">✻</span> /* six-pointed star */

<style>
.snowflake {
  font-size: var(--size);
  color: white;
  background: transparent;
  border-radius: 0;
  width: auto;
  height: auto;
}
</style>

Gradient Background Blur

Subtler snow-feel without particles: radial gradients at top fade into a cool blue, suggesting winter atmosphere without the computational cost of animation.

When to Switch to Canvas

CSS-based snow hits limits around 100-150 particles. For more realistic effects:

  • Many particles (200+) — Canvas 2D or WebGL handles thousands smoothly
  • Real physics — wind forces, collision, accumulation on surfaces
  • Snow buildup — particles that stop and pile up at the bottom or on elements
  • Interactive snow — particles that react to mouse position
  • Non-circular detailed flakes — custom PNG textures or procedural SVG flakes

Libraries like Particles.js, tsparticles, or custom Canvas code cover these cases. Use CSS snow only when the effect is subtle and decorative — Canvas when it's a featured visual.

Enabling Seasonally

/* Show snow only December and January */
<script>
  const month = new Date().getMonth(); /* 0-11 */
  if (month === 11 || month === 0) {
    document.body.classList.add('snowing');
  }
</script>

<style>
.snow { display: none; }
body.snowing .snow { display: block; }
</style>

Frequently Asked Questions

Does snowfall hurt SEO or performance scores?
Moderate CSS snow (30-50 particles) has negligible impact on Core Web Vitals. Heavy snow (100+ particles with wobble) can drop INP scores on mobile. Test with Lighthouse before launching — if the score drops, reduce particle count or use prefers-reduced-motion to disable on request.
Can I use CSS snow on every page or just the homepage?
Homepage and landing pages only. Persistent snow on every page of a site becomes annoying quickly — users stop noticing it, and it just consumes battery. Add a dismiss option if you want it sitewide.
What other seasonal effects work with pure CSS?
Falling leaves (autumn) — use leaf emoji and wider horizontal drift. Rain — thin long white rectangles falling faster. Confetti — colorful squares in various sizes. Fireworks — harder in CSS, usually needs Canvas.
How do I prevent snow from showing on mobile?
Use media queries: @media (max-width: 640px) { .snow { display: none; } }. Saves battery on mobile where animations are more expensive. Alternatively, reduce the particle count on mobile rather than removing entirely.
Can I capture snow over specific elements only?
Yes — put the snow container inside the specific section (not position: fixed, use position: absolute relative to the section). Adjust the fall height to match the section's height. Useful for hero sections with snow only in the banner area.

Free reference

The 47 Tools Cheat Sheet

One printable page with every tool on this site, what it does, and when to reach for it. Free. It asks for an email so Gumroad can send you the file, unlike the tools themselves.