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 slightly —
filter: 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
transformfor movement — nevertoporleft, which trigger layout. - Add
will-change: transformto 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-motionto 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, useposition: absoluterelative to the section). Adjust the fall height to match the section's height. Useful for hero sections with snow only in the banner area.