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.