🚀

Update available

We just released a new resource or update, refresh the Vault to access the latest version.

Cancel

Refresh now

Harri

Profile Picture

Harri

Lemke

Snowflake Effect

Documentation

Webflow

Code

Setup: External Scripts

HTML

Copy
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15/dist/gsap.min.js"></script>

Step 1: Add HTML

HTML

Copy
<div data-snowflake-container data-strength="4" data-infinite="true" class="snowflake-container">
  <div data-snowflake class="snowflake-el hidden"></div>
</div>

Step 2: Add CSS

CSS

Copy
.snowflake-container {
  z-index: 100;
  pointer-events: none;
  width: 100%;
  height: 100vh;
  position: fixed;
  inset: 0%;
  overflow: hidden;
}

.snowflake-el {
  aspect-ratio: 1 / 1.15;
  background-image: url('https://cdn.prod.website-files.com/6941599afc835c41f83ca9ca/69416a9de9533b6332c72b9e_snowflake.avif');
  background-position: 50%;
  background-repeat: no-repeat;
  background-size: contain;
  width: 1.5em;
  position: absolute;
}

.snowflake-el.hidden {
  opacity: 0;
}

Step 2: Add Javascript

Step 3: Add Javascript

Javascript

Copy
function initSnowflakeEffect() {
  const container = document.querySelector("[data-snowflake-container]");
  if (!container) return;

  // Prevent double init
  if (container.dataset.snowRunning === "true") return;
  container.dataset.snowRunning = "true";

  const templates = Array.from(container.querySelectorAll("[data-snowflake]"));
  if (!templates.length) {
    console.warn("initSnowflakeEffect: No [data-snowflake] element found");
    container.dataset.snowRunning = "false";
    return;
  }

  const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
  const strength = clamp(parseInt(container.dataset.strength ?? "4", 10) || 0, 0, 10);
  const infinite = (container.dataset.infinite ?? "true") !== "false";

  // Configuration
  const durationMin = 8;
  const durationMax = 12;
  const scaleMin = 0.3;
  const scaleMax = 1.2;
  const opacityMin = 0.2;
  const opacityMax = 1.0;

  // Strength affects how many + how fast
  const spawnRate = gsap.utils.mapRange(0, 10, 0.15, 5.0, strength); // flakes/sec
  const maxOnScreen = Math.round(gsap.utils.mapRange(0, 10, 12, 180, strength));
  const burstCount = Math.round(gsap.utils.mapRange(0, 10, 10, 160, strength)); // if infinite=false

  let running = true;
  let activeCount = 0;
  let scheduledCall = null;
  let burstSpawned = 0;

  const getHeight = () => container.clientHeight || window.innerHeight;

  function stop(removeExisting = true) {
    running = false;
    container.dataset.snowRunning = "false";
    if (scheduledCall) scheduledCall.kill();

    if (removeExisting) {
      container.querySelectorAll(".snowflake-el.is-spawned").forEach(el => el.remove());
      activeCount = 0;
    }
  }

  function cleanupFlake(flake, tweens) {
    tweens.forEach(t => t && t.kill());
    flake.remove();
    activeCount--;

    // In one-burst mode: when everything spawned AND are all done, stop.
    if (!infinite && burstSpawned >= burstCount && activeCount <= 0) {
      stop(false);
    }
  }

  function spawnOne() {
    if (!running) return;
    if (activeCount >= maxOnScreen) return;
  
    const tpl = templates[Math.floor(Math.random() * templates.length)];
    const flake = tpl.cloneNode(true);
  
    flake.classList.remove("hidden");
    flake.classList.add("is-spawned");
    flake.style.willChange = "transform, opacity";
  
    const scale = gsap.utils.random(scaleMin, scaleMax, 0.001);
    const duration = gsap.utils.random(durationMin, durationMax, 0.001);
  
    // Wave-ish drift
    const baseSway = gsap.utils.random(12, 60, 0.1);
    const sway = baseSway * (0.6 + strength / 20);
  
    // Choose left so that drifting doesn't bias distribution (keep within bounds)
    const containerWidth = container.clientWidth || window.innerWidth;
    const swayPct = (sway / containerWidth) * 100; // sway in % of container width
    const padPct = Math.min(20, Math.max(0, swayPct)); // clamp padding
    flake.style.left = `${gsap.utils.random(padPct, 100 - padPct, 0.1)}%`;
  
    flake.style.opacity = gsap.utils.random(opacityMin, opacityMax, 0.001);
  
    container.appendChild(flake);
    activeCount++;
  
    const h = getHeight();
    const startY = -gsap.utils.random(30, Math.min(180, h * 0.25), 1);
    const endY = h + gsap.utils.random(30, Math.min(220, h * 0.35), 1);
  
    const xStart = gsap.utils.random(-sway, sway, 0.1);
    const xEnd = -xStart;
    const swayDur = gsap.utils.random(1.6, 3.8, 0.001);
  
    // Gentle rotation wobble
    const rotStart = gsap.utils.random(-12, 12, 0.1);
    const rotEnd = gsap.utils.random(-28, 28, 0.1);
    const rotDur = gsap.utils.random(2.2, 5.0, 0.001);
  
    let fallTween, swayTween, rotTween, fadeTween;
  
    fallTween = gsap.fromTo(
      flake,
      { y: startY, xPercent: -50, scale, rotate: rotStart },
      {
        y: endY,
        xPercent: -50,
        ease: "none",
        duration,
        onComplete: () => cleanupFlake(flake, [fallTween, swayTween, rotTween, fadeTween]),
      }
    );
  
    const swayRepeats = Math.max(1, Math.floor(duration / swayDur));
    swayTween = gsap.fromTo(
      flake,
      { x: xStart },
      { x: xEnd, ease: "sine.inOut", duration: swayDur, repeat: swayRepeats, yoyo: true }
    );
  
    const rotRepeats = Math.max(1, Math.floor(duration / rotDur));
    rotTween = gsap.fromTo(
      flake,
      { rotate: rotStart },
      { rotate: rotEnd, ease: "sine.inOut", duration: rotDur, repeat: rotRepeats, yoyo: true }
    );
  
    fadeTween = gsap.to(flake, {
      opacity: 0,
      duration: 1,
      ease: "power1.out",
      delay: Math.max(0, duration - 1),
    });
  }

  function scheduleNext() {
    if (!running) return;

    const avgGap = 1 / spawnRate;
    const nextIn = gsap.utils.random(avgGap * 0.6, avgGap * 1.4, 0.001);

    scheduledCall = gsap.delayedCall(nextIn, () => {
      spawnOne();
      scheduleNext();
    });
  }

  if (infinite) {
    const seedCount = Math.round(gsap.utils.mapRange(0, 10, 6, 60, strength));
    for (let i = 0; i < seedCount; i++) {
      gsap.delayedCall(gsap.utils.random(0, 1.2, 0.001), spawnOne);
    }
    scheduleNext();
  } else {
    for (let i = 0; i < burstCount; i++) {
      burstSpawned++;
      gsap.delayedCall(gsap.utils.random(0, 2.0, 0.001), spawnOne);
    }
  }
}

// Initialize Snowflake Effect
document.addEventListener("DOMContentLoaded", () => {
  initSnowflakeEffect();
});

Implementation

Container

Add a wrapper using [data-snowflake-container] to define the bounds where snowflakes spawn and animate.

Snowflake

Place a single 'template' element using [data-snowflake] inside the container so the script can clone it for each spawned flake.

Strength

Use [data-strength="4"] (default 4, clamped between 0–10) to control overall intensity by scaling spawn rate, on-screen limit, and seeding.

Infinite

Set [data-infinite="true"] (default true) to keep spawning continuously, or set it to false to spawn one burst and stop once all flakes have finished.

Duration

Adjust durationMin and durationMax in the script to define how long each snowflake takes to fall from top to bottom, with values randomly chosen per flake.

Scale

Adjust scaleMin and scaleMax in the script to control the random size range applied to each snowflake as it spawns.

Opacity

Adjust opacityMin and opacityMax in the script to control the random starting opacity range of each snowflake.

Spawn Rate

The internal spawnRate value maps [data-strength] to flakes-per-second, determining how frequently new snowflakes are spawned.

Max On Screen

The internal maxOnScreen limit maps [data-strength] to a cap on how many active snowflakes can exist at once.

Burst Count

The internal burstCount maps [data-strength] to the total number of flakes spawned when [data-infinite="false"] is used.

Sway

The internal sway calculation increases horizontal drift based on [data-strength], creating wider left-right motion at higher values.

Seed

When [data-infinite="true"] is enabled, the internal seed count spawns an initial batch of snowflakes on load, scaled by [data-strength].

Live preview

Osmo Robot AI

Copy context for AI

Beta

Webflow

HTML/CSS/JS

Save video

Copy share link

Resource details

  • Published

    December 17, 2025

  • Category

    Gimmicks

  • Popularity

    475 visitors

  • Need help?

    Join Slack

Background
Falling
Gravity
Movement
Ilja van EckIlja van Eck

Creator Credits

We always strive to credit creators as accurately as possible. While similar concepts might appear online, we aim to provide proper and respectful attribution.

s