The Falling Snow Effect

by kirupa   |   filed under Web Animation

For most of my life, I grew up in parts of the world where you never really had snow. That wasn't fun. During Christmas, the images of wintery goodness you grew up expecting and the sunny and warm weather outside your window never made sense. To make up for the lack of snow in my real world, I simulated falling snow on my computer. It was one of the first animations I created using ActionScript in Flash many years ago, and below is a variation of the original effect running in sweet HTML, CSS, and JavaScript:

What you should see are snowflakes slowly zigging and zagging as they fall from the virtual sky. In this article, you will learn two things. You will first learn how to quickly add this effect to your own content. Second, if you are interested in learning how this effect works, I provide a high-level deconstruction of all the markup and code that goes into making this effect work.

Onwards!

Adding This Effect

To add some falling snow to your own HTML document, you just have to copy and paste some stuff. Towards the bottom of your page (probably just above the closing </body> tag), add all of the following:

<style>
  #snowflakeContainer {
    position: absolute;
    left: 0px;
    top: 0px;
    display: none;
  }

  .snowflake {
    position: fixed;
    background-color: #CCC;
    user-select: none;
    z-index: 1000;
    pointer-events: none;
    border-radius: 50%;
    width: 10px;
    height: 10px;
  }
</style>

<div id="snowflakeContainer">
  <span class="snowflake"></span>
</div>

<script>
  // Array to store our Snowflake objects
  var snowflakes = [];

  // Global variables to store our browser's window size
  var browserWidth;
  var browserHeight;

  // Specify the number of snowflakes you want visible
  var numberOfSnowflakes = 50;

  // Flag to reset the position of the snowflakes
  var resetPosition = false;

  // Handle accessibility
  var enableAnimations = false;
  var reduceMotionQuery = matchMedia("(prefers-reduced-motion)");

  // Handle animation accessibility preferences 
  function setAccessibilityState() {
    if (reduceMotionQuery.matches) {
      enableAnimations = false;
    } else { 
      enableAnimations = true;
    }
  }
  setAccessibilityState();

  reduceMotionQuery.addListener(setAccessibilityState);

  //
  // It all starts here...
  //
  function setup() {
    if (enableAnimations) {
      window.addEventListener("DOMContentLoaded", generateSnowflakes, false);
      window.addEventListener("resize", setResetFlag, false);
    }
  }
  setup();

  //
  // Constructor for our Snowflake object
  //
  function Snowflake(element, speed, xPos, yPos) {
    // set initial snowflake properties
    this.element = element;
    this.speed = speed;
    this.xPos = xPos;
    this.yPos = yPos;
    this.scale = 1;

    // declare variables used for snowflake's motion
    this.counter = 0;
    this.sign = Math.random() < 0.5 ? 1 : -1;

    // setting an initial opacity and size for our snowflake
    this.element.style.opacity = (.1 + Math.random()) / 3;
  }

  //
  // The function responsible for actually moving our snowflake
  //
  Snowflake.prototype.update = function () {
    // using some trigonometry to determine our x and y position
    this.counter += this.speed / 5000;
    this.xPos += this.sign * this.speed * Math.cos(this.counter) / 40;
    this.yPos += Math.sin(this.counter) / 40 + this.speed / 30;
    this.scale = .5 + Math.abs(10 * Math.cos(this.counter) / 20);

    // setting our snowflake's position
    setTransform(Math.round(this.xPos), Math.round(this.yPos), this.scale, this.element);

    // if snowflake goes below the browser window, move it back to the top
    if (this.yPos > browserHeight) {
      this.yPos = -50;
    }
  }

  //
  // A performant way to set your snowflake's position and size
  //
  function setTransform(xPos, yPos, scale, el) {
    el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0) scale(${scale}, ${scale})`;
  }

  //
  // The function responsible for creating the snowflake
  //
  function generateSnowflakes() {

    // get our snowflake element from the DOM and store it
    var originalSnowflake = document.querySelector(".snowflake");

    // access our snowflake element's parent container
    var snowflakeContainer = originalSnowflake.parentNode;
    snowflakeContainer.style.display = "block";

    // get our browser's size
    browserWidth = document.documentElement.clientWidth;
    browserHeight = document.documentElement.clientHeight;

    // create each individual snowflake
    for (var i = 0; i < numberOfSnowflakes; i++) {

      // clone our original snowflake and add it to snowflakeContainer
      var snowflakeClone = originalSnowflake.cloneNode(true);
      snowflakeContainer.appendChild(snowflakeClone);

      // set our snowflake's initial position and related properties
      var initialXPos = getPosition(50, browserWidth);
      var initialYPos = getPosition(50, browserHeight);
      var speed = 5 + Math.random() * 40;

      // create our Snowflake object
      var snowflakeObject = new Snowflake(snowflakeClone,
        speed,
        initialXPos,
        initialYPos);
      snowflakes.push(snowflakeObject);
    }

    // remove the original snowflake because we no longer need it visible
    snowflakeContainer.removeChild(originalSnowflake);

    moveSnowflakes();
  }

  //
  // Responsible for moving each snowflake by calling its update function
  //
  function moveSnowflakes() {

    if (enableAnimations) {
      for (var i = 0; i < snowflakes.length; i++) {
        var snowflake = snowflakes[i];
        snowflake.update();
      }      
    }

    // Reset the position of all the snowflakes to a new value
    if (resetPosition) {
      browserWidth = document.documentElement.clientWidth;
      browserHeight = document.documentElement.clientHeight;

      for (var i = 0; i < snowflakes.length; i++) {
        var snowflake = snowflakes[i];

        snowflake.xPos = getPosition(50, browserWidth);
        snowflake.yPos = getPosition(50, browserHeight);
      }

      resetPosition = false;
    }

    requestAnimationFrame(moveSnowflakes);
  }

  //
  // This function returns a number between (maximum - offset) and (maximum + offset)
  //
  function getPosition(offset, size) {
    return Math.round(-1 * offset + Math.random() * (size + 2 * offset));
  }

  //
  // Trigger a reset of all the snowflakes' positions
  //
  function setResetFlag(e) {
    resetPosition = true;
  }
</script>

What you just pasted contains everything you would need to generate our snowflakes and cause them to animate and fall to the ground.

Once you've added all of this HTML, CSS, and JavaScript, preview in your browser to make sure everything works as expected. If for whatever reason things aren't working properly, take a look at the source in the example page and see how I have everything setup. If you still have questions, quickly post on the forums and I or somebody else will help you out.

Out of the box, the animation may be perfect for your needs. If you want to make some changes to it, there are two customizations that you can make easily. The first is changing the color of the snowflake. The color of our snowflake is defined inside the .snowflake class in our CSS:

.snowflake {
  position: fixed;
  background-color: #CCC;
  user-select: none;
  z-index: 1000;
  pointer-events: none;
  border-radius: 50%;
  width: 10px;
  height: 10px;
}

Change the background-color property to whatever color you want your snowflake to be. In our example, I have it set to #FFFFFF (white) because the background is darker. If you are going to be using this effect on a lighter colored background, you should adjust the snowflake color accordingly.

The second customization you can make revolves around the number of snowflakes that make up this effect. The more snowflakes you have, the more of a wintery storm feeling your page will have. The fewer snowflakes you have, well...that will just look weak! To adjust the number of snowflakes, find the numberOfSnowflakes variable in our JavaScript:

var numberOfSnowflakes = 50;

It is set to 50 currently. Replace it to a higher or lower number depending on the sort of effect you are looking for. One thing to keep in mind is performance. The more snowflakes you add, the more sluggish your page will be on older hardware. Don't let that warning deter you, though. If your audience will tend to be on more powerful hardware (both mobile and desktop), then you should feel free to go crazy!

And with that, you are done with having a simple way to get snow all over your HTML content. If that's all you came here for, you are free to go and enjoy the rest of the day...or night. Now, I do recommend you take some time and learn how this effect works. Besides giving you some great things to talk about during dinner with friends and/or family, learning how this effect works will help you further your web animation-fu. One can never have too much web animation-fu!

Overview of How this Effect Works

The way this effect works is pretty simple...sort of. You have many snowflakes (aka our elements), and each element's position and size is changed slightly many MANY times each second. This change is what results in the animation we ultimately see. The starting point for all this is found in our HTML where have a single snowflake element defined. It's a circle in our case, but for illustration purposes I'll use the following snowflake-looking shape instead:

Using code, that single snowflake is cloned a bunch of times (as determined by what our numberOfSnowflakes variable says):

As part of cloning our snowflakes, we make some minor changes to their appearance so that they all look a little bit different from each other:

Once we have our (mostly) unique snowflakes, the rest of the effort is just taking the well-trodden path towards getting them to each move slightly differently. We will cover the code and everything in some detail in the next few sections, so roll up your sleeves, get some coffee, and let's get started!

Deeper Look at How the Effect Works

Now that we have a very basic understanding of how this effect works, let's shift gears and focus on how this effect is implemented. Let's start by looking at our HTML:

<div id="snowflakeContainer">
  <span class="snowflake"></span>
</div>

There isn't much going on here. We have one element called snowflakeContainer, and it has one child with a class value of snowflake. That is the entirety of the HTML we explicitly specify in our document. To turn this HTML into the falling snowflake-like effect that you see, we have to take a look at our JavaScript. That's where a lot of the heavy lifting gets done.

The Starting Point

When it comes to our JavaScript, the setup function is what gets called by our code first:

function setup() {
  if (enableAnimations) {
    window.addEventListener("DOMContentLoaded", generateSnowflakes, false);
    window.addEventListener("resize", setResetFlag, false);
  }
}
setup();

We first check if animations are enabled. After that, the big thing we do here is listen for two events. When the DOMContentLoaded event is fired, we call generateSnowflakes. If the window is resized, we call setResetFlag. Of these two events, what happens when our page's DOM has loaded is the most interesting. We will look at that code next.

Generating the Snowflakes

A big part of this effect is taking the one snowflake we have defined in our HTML and using it as a template for the many snowflakes we ultimately end up seeing. The code for making this happen lies in the generateSnowflakes function:

function generateSnowflakes() {

  // get our snowflake element from the DOM and store it
  var originalSnowflake = document.querySelector(".snowflake");

  // access our snowflake element's parent container
  var snowflakeContainer = originalSnowflake.parentNode;
  snowflakeContainer.style.display = "block";

  // get our browser's size
  browserWidth = document.documentElement.clientWidth;
  browserHeight = document.documentElement.clientHeight;

  // create each individual snowflake
  for (var i = 0; i < numberOfSnowflakes; i++) {

    // clone our original snowflake and add it to snowflakeContainer
    var snowflakeClone = originalSnowflake.cloneNode(true);
    snowflakeContainer.appendChild(snowflakeClone);

    // set our snowflake's initial position and related properties
    var initialXPos = getPosition(50, browserWidth);
    var initialYPos = getPosition(50, browserHeight);
    var speed = 5 + Math.random() * 40;

    // create our Snowflake object
    var snowflakeObject = new Snowflake(snowflakeClone,
      speed,
      initialXPos,
      initialYPos);
    snowflakes.push(snowflakeObject);
  }

  // remove the original snowflake because we no longer need it visible
  snowflakeContainer.removeChild(originalSnowflake);

  moveSnowflakes();
}

This code does a bunch of things:

  1. Creates a clone of our snowflake in the DOM
  2. Sets the random initial snowflake position (based on the current viewport size)
  3. Sets the random speed our snowflake will fall in
  4. Stores everything in a JavaScript representation of our snowflake
  5. Kicks off the animation

All of these steps are repeated for each snowflake that we create. One output of all this code running is a new Snowflake object:

var snowflakeObject = new Snowflake(snowflakeClone,
  speed,
  initialXPos,
  initialYPos);
snowflakes.push(snowflakeObject);

This object not only carries with it a reference to the DOM representation of a snowflake element, it also carries the values for the speed and position that were generated a few lines earlier:

We'll cover this object in greater detail later, but this Snowflake object and its associated DOM element make up our snowflake from a code point of view. Getting back to the present, after our snowflake is created, we keep this object around for later use by stashing it away in an array called snowflakes. All of this happens inside a loop that runs for each snowflake we want as part of the effect.

The last thing that happens in our generateSnowflakes function is the call to moveSnowflakes where all of the snowflakes we generated earlier be set to move. We'll find out next.

The Animation Loop

In every animation that relies on JavaScript, we have something known as the animation loop that is responsible for running repeatedly and containing any code needed to animate something around. The moveSnowflakes function is that loop for us:

function moveSnowflakes() {

  if (enableAnimations) {
    for (var i = 0; i < snowflakes.length; i++) {
      var snowflake = snowflakes[i];
      snowflake.update();
    }      
  }

  // Reset the position of all the snowflakes to a new value
  if (resetPosition) {
    browserWidth = document.documentElement.clientWidth;
    browserHeight = document.documentElement.clientHeight;

    for (var i = 0; i < snowflakes.length; i++) {
      var snowflake = snowflakes[i];

      snowflake.xPos = getPosition(50, browserWidth);
      snowflake.yPos = getPosition(50, browserHeight);
    }

    resetPosition = false;
  }

  requestAnimationFrame(moveSnowflakes);
}

We can see that by looking at the requestAnimationFrame call at the bottom that will call the moveSnowflakes function repeatedly. Inside our animation loop, the main thing that happens is our code for updating each of our snowflake:

if (enableAnimations) {
  for (var i = 0; i < snowflakes.length; i++) {
    var snowflake = snowflakes[i];
    snowflake.update();
  }      
}

We go through every snowflake stored by our snowflakes array and call the update method on it. If you were expecting something more fancy, here ain't it. The fancy stuff lives in our Snowflake code, so let's go there next.

The Snowflake Implementation

All of the complexity for making our snowflake animate lives inside the definition for our Snowflake object. When we first create our snowflakes via the generateSnowflakes function, this is the code that gets called:

function Snowflake(element, speed, xPos, yPos) {
  // set initial snowflake properties
  this.element = element;
  this.speed = speed;
  this.xPos = xPos;
  this.yPos = yPos;
  this.scale = 1;

  // declare variables used for snowflake's motion
  this.counter = 0;
  this.sign = Math.random() < 0.5 ? 1 : -1;

  // setting an initial opacity and size for our snowflake
  this.element.style.opacity = (.1 + Math.random()) / 3;
}

When our snowflake gets created, all we do is store a bunch of properties on it to represent the various characteristics we want it to maintain. Some of the properties we specify as part of creating the snowflake and provide as arguments, but other properties we define for internal use like counter, scale, and sign.

The important detail to keep in mind is that these properties aren't designed to stay still. They start off with the initial values they get when the snowflake is created. When the update method (that our moveSnowflakes animation loop calls) runs, these properties change and play a major role in defining how the snowflake moves. The update method looks as follows:

Snowflake.prototype.update = function () {
  // using some trigonometry to determine our x and y position
  this.counter += this.speed / 5000;
  this.xPos += this.sign * this.speed * Math.cos(this.counter) / 40;
  this.yPos += Math.sin(this.counter) / 40 + this.speed / 30;
  this.scale = .5 + Math.abs(10 * Math.cos(this.counter) / 20);

  // setting our snowflake's position
  setTransform(Math.round(this.xPos), 
               Math.round(this.yPos), 
               this.scale, 
               this.element);

  // if snowflake goes below the browser window, move it back to the top
  if (this.yPos > browserHeight) {
    this.yPos = -50;
  }
}

The bulk of this code helps our snowflakes oscillate as they make their way to the ground. This oscillation is made possible by using the sin and cos trigonmetric functions along with some numbers that scale up or down the intensity of the movement. The counter property is incremented here as well, and this rate of incrementing determines how quickly our snowflakes animate. If you are up for it, you can adjust the values and numbers you see here to see how your snowflake's movements will be affected.

All of these numerical changes are still in the JavaScript realm. They are a part of our Snowflake object, but they don't actually affect the snowflake DOM element we see on the screen. That gap is addressed by our call to the setTransform function:

setTransform(Math.round(this.xPos), 
             Math.round(this.yPos), 
             this.scale, 
             this.element);

We will take a look at what it does next!

Setting our Snowflake's Position and Size

The setTransform function takes all the calculations made by our snowflake's update method and translates them into the position and size values that we ultimately see:

function setTransform(xPos, yPos, scale, el) {
  el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0) scale(${scale}, ${scale})`;
}

The way this we do this is by setting the transform property on the DOM element that represents each snowflake. We set both the translate3d function to performantly adjust the horizontal and vertical position, and we also set the scale function to adjust how big or small the snowflake is as it is falling down. Once this line of code has executed, one frame of our animation is complete with our snowflake having moved ever so slightly towards the ground.

Accessibility and Animations

Throughout our code, you may have seen us checking the value of an enableAnimations property. This property's value corresponds to whether the user has disabled animations as part of an accessibility setting. The Toggling Animations On and Off tutorial goes into more detail on this, but there are users who, when seeing animations like our falling snow, can get sick or nauseus. There is an accessibility setting most operating systems provide to let users reduce the animations they see across everything they do, and we can respect that setting as part of animations we create.

The code that sets enableAnimations looks as follows:

// Handle accessibility
var enableAnimations = false;
var reduceMotionQuery = matchMedia("(prefers-reduced-motion)");

// Handle animation accessibility preferences 
function setAccessibilityState() {
  if (reduceMotionQuery.matches) {
    enableAnimations = false;
  } else { 
    enableAnimations = true;
  }
}
setAccessibilityState();

reduceMotionQuery.addListener(setAccessibilityState);

The system accessibility preference for reducing animation is represented by the prefers-reduced-motion media query feature. What we do here is poll for that feature's value and set the enableAnimations property to specify whether we play the falling snow animation or we don't.

Constrain the Falling Snow to a Particular Region

Our falling snow effect currently goes over the entire page. You may not want that. You may just want the snow effect to appear over just a particular region - like an image, a banner, etc. To learn how to modify this effect to achieve that, check out Kyle Murray's excellent Constraining the Falling Snow Effect tutorial.

Conclusion

So, there you have it - a look at how HTML, CSS, and JavaScript come together to create the animated falling snow effect that you see. If you've been following the animation tutorials on this site, most of what you saw here should have been a review. What isn't a review is how everything comes together to create the final effect that you see. That is always the most fun as well as the most tricky part of combining all of the various individual tips and techniques we tend to go deep on. If you do want to go deep on any subtopic this effect exposes, read the following tutorials: Creating, Removing, and Cloning Elements, Animating with requestAnimationFrame, Animating Many Things, Toggling Animations On and Off, Running Your Code at the Right Time, Generating Random Numbers, Animating Movement Smoothly Using CSS, A Deeper Look at Objects, Introduction to Easing, and a few more!

If you have a question about this or any other topic, the easiest thing is to drop by our forums where a bunch of the friendliest people you'll ever run into will be happy to help you out!

THE KIRUPA NEWSLETTER

Get cool tips, tricks, selfies, and more...personally hand-delivered to your inbox!

( View past issues for an idea of what you've been missing out on all this time! )

GOT A QUESTION?

HOT FORUM TOPICS

Serving you freshly baked content since 1998!

Killer hosting by (mt) mediatemple

Facebook Twitter Youtube Pinterest Instagram Github
BACK TO TOP
new books - yay!!!