falling snow in a div

Kirupa wrote a very popular tutorial on how to make it look like snow is falling within a webpage. People frequently ask questions about how to stuff the effect inside of a div, presumably to contain it within a header, banner, etc. I’ll write about how to do that below.

strategic considerations

The approach Kirupa uses to make the snow move around is to update absolutely-positioned divs containing an asterisk. It’s a problematic approach when trying to create the effect of bounded off snowflakes. He actually gets around this on the tutorial page by embedding the entire example in an iframe. Masking is key to making it look like the snowflakes are trapped within a specific bounding box, since the flakes will gradually be sliced off-screen as they pass any edge. The alternative would be to make the snowflakes bounce off of the edge or disappear immediately, but those would both look unnatural.

So, we need to find a way to recreate that masking effect within a div. What are the options?

frames

Like Kirupa’s own tutorial, we could just use an iframe. I name this approach first because I suspect some of the people asking for snow in a div would be happy with this solution if it was explicitly pointed out to them rather than it just being Kirupa’s de facto way of displaying his example/demo results. On the other hand, it’s not a great solution to retrofit onto existing site architectures, because this is just supposed to be an overlay of snow, which shouldn’t require tinkering with any sort of content-to-HTML pipeline. So I wouldn’t recommend using a frame for this effect unless you have that level of control over your whole site and want to commit to an unconventional approach.

CSS mask

CSS tempts us with several candidates, like css-masking, clip, and clip-path. None of the elements of css-masking are supported in any version of IE, so that pretty much immediately kills it as a viable thing to suggest to most web developers (a polyfill for that would be huge, I imagine). clip is deprecated, and nobody from MDN has gotten around to testing its mobile browser compatibility, so it’s pretty suspect. clip-path is apparently coming with css-masking, or can already be applied within an embedded SVG document.

SVG

SVG seems promising because it is rendered in HTML as a sort of embedded, standalone document. It’s somewhat like an iframe in that sense, but you can include nested SVG elements right in your HTML, rather than having to refer to a separate file. The key benefit is automatic edge masking, without even having to use SVG’s internal masking features. And it’s all vectors, so you don’t have to worry about artifacts of rasterization.

canvas

HTML5’s canvas is the poster child for Flash-style contained drawing, so we know that it’ll mask off snowflakes properly. It’s easy to position a canvas over a div, but my initial hunch is that there will be problems with pointer events not reaching content below, which was something that Kirupa’s original approach permits. You can use pointer-events: none to disable any interaction with the canvas, which might be what you want. On the other hand, this makes it harder to react to the situation where you want people to touch the snowflakes (say, to have them disappear or bounce around) while still letting people interact with the underlying content.

two implementations, svg and canvas

The first thing we’ll change in Kirupa’s code, regardless of whether we’re choosing canvas or SVG, is anything that references the browser’s width or height. Remember, we’re now targeting a single div (or other block element). I replaced the global variables browserWidth and browserHeight with a single variable named targetElement, and I initialized it with the assignment targetElement = document.querySelector('#content'). Thus, our target element is the div IDed content, which is the Christmas wish list and it’s heading (but not the rest of the blank space on the page). In your own code, this target element could be whichever div you’d like! I then replaced all of the occurrences of the browserXXXX variables with targetElement.clientWidth or targetElement.clientHeight, depending on which dimension made sense. I eliminated some one-time-use variables that Kirupa had in the process of doing this, but doing so is optional.

You’ll also want to declare a global variable named overlayElement right by targetElement, which we’ll use to either hold an svg or canvas element.

Next, change this declaration:

function setTranslate3DTransform(element, xPosition, yPosition)

to this:

function setPosition(element, x, y)

… and don’t forget to change its name the one place it’s called: inside Snowflake.prototype.update.

Inside generateSnowflakes, we’ll make overlayElement be the right thing:

overlayElement = document.querySelector('#snowflakeContainer')

On the next line, we’ll call a newly-defined function I named positionOverlay:

// position the overlay over the target div/element
function positionOverlay(){
    var targetRect = targetElement.getBoundingClientRect();
    overlayElement.style.top = targetRect.top;
    overlayElement.style.left = targetRect.left;
    overlayElement.setAttribute('width', targetRect.width);
    overlayElement.setAttribute('height', targetRect.height);
}

I broke it out into a separate function because it needs to be called again if the div is resized. Remove the corresponding #snowflakeContainer top and left rules in the CSS, since we’re setting that value here instead.


svg

(follow these instructions if you want to use SVG, skip ahead to the canvas section otherwise)

We first want to replace the old snowflake-containing div:

<div id="snowflakeContainer">
    <p class="snowflake">*</p>
</div>

…with some suspiciously similar-looking SVG:

<svg id="snowflakeContainer">
    <text class="snowflake" fill="white">*</text>
</svg>

We also need to change the body of setPosition to:

element.setAttribute('x', x);
element.setAttribute('y', y);

Arguably you could do that inline where setPosition is called, but this seems okay for maintaining some parallels with the original code. SVG has a 2D translate function, but it sets values relatively, so if you repeated call transform = "translate(1 1)" in SVG lingo, your element will move diagonally by one pixel each frame (this is an implicit form of +=). This is at odds with Kirupa’s existing code infrastructure, which assumes that Snowflake instances keep track of their position independently. So we’ll just set X and Y coordinates to avoid diverging from the logic you’re familiar with from Kirupa’s tutorial.

At this point, things should basically be working! Test it out!.

Despite some fiddling with viewBoxes and preserveAspectRatio, I noticed my snowflakes weren’t going down far enough before they reset, so I tacked on a + 50 to the conditional expression that guards resets, resulting in this:

// if snowflake goes below the bottom of the target element, move it back to the top
    if (this.yPos > targetElement.clientHeight + 50)

… and resulting in the masked resets we really want.

Try switching to another element by switching up how targetElement is initialized:

targetElement = document.querySelectorAll('li')[2]

Neat! Now the snow only falls in an extremely narrow band.

If you want to be able to select underlying elements when there isn’t a snowflake in the way, add pointer-events: visibleFill to .snowflake, and pointer-events: none to #snowflakeContainer in the CSS section of your HTML.


canvas

We first want to replace the old snowflake-containing div:

<div id="snowflakeContainer">
    <p class="snowflake">*</p>
</div>

…with a canvas!:

<canvas id="snowflakeContainer"></canvas>

Because we’re no longer using HTML elements as a base for our snowflake symbol, we can remove these sets of lines in generateSnowflakes:

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

// 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;

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

This does mean that we need to change what the snowflake thinks its element is:

var snowflakeObject = new Snowflake(overlayElement.getContext('2d'), 
                                            speed, 
                                            initialXPos, 
                                            initialYPos);

Since we’re using a single canvas for all snowflakes, this isn’t different for each instance. We also want to set the opacity and fontSize directly on each Snowflake instance, rather than through CSS. Do this in the Snowflake constructor function:

this.opacity = .1 + Math.random();
this.fontSize = 12 + Math.random() * 50 + "px";

(That ends up being just removing the middle part of some lines that are already there.)

At the beginning of moveSnowflakes, add this code:

var ctx = overlayElement.getContext('2d');
ctx.clearRect(0, 0, overlayElement.clientWidth, overlayElement.clientHeight);

Because canvas uses immediate mode-style rendering, we need to clear and redraw every snowflake every frame.

Now we need to change the code that updates each snowflake’s position. Start by changing the setPosition call inside of Snowflake.prototype.update to this:

setPosition(this, Math.round(this.xPos), Math.round(this.yPos));

We’re passing something different as the element here because we need some information stored directly on the snowflake itself. You can see how this plays out when you change the body of setPosition to this:

var size = element.fontSize;
element.element.font = size + ' Cambria, Georgia, serif';
element.element.fillText('*', x, y, 50);
element.element.fillStyle = 'rgba(255, 255, 255, ' + element.opacity + ')';

It may help to rename the element parameter, because we’re kind of abusing its intended meaning. That’s okay, because we’re adapting this from older code, but you might want to rename the parameter to keep your sanity and reflect how it’s really being used:

function setPosition(flake, x, y) {
    var size = flake.fontSize;
    flake.element.font = size + ' Cambria, Georgia, serif';
    flake.element.fillText('*', x, y, 50);
    flake.element.fillStyle = 'rgba(255, 255, 255, ' + flake.opacity + ')';
}

Just like in the SVG implementation, we’re going to make a slight tweak to an if statement so that the snowflakes fall down far enough before resetting (in update):

if (this.yPos > targetElement.clientHeight + 50)

If you want to be able to select underlying elements like text or links, add pointer-events: none to #snowflakeContainer in the CSS section of your HTML. Unlike the SVG approach, this is an all-or-nothing choice when it comes to choosing what gets pointer events.

And that’s it! You have falling snow contained to a div or element of your choice!


conclusion & live, complete examples

There you have it, falling snow in box. Here are some complete examples you can use to copy the source directly and see both implementations in action:

Hope you enjoyed this augmentation of a great tutorial, cya around the forums.