Skip to main content

The drifting snowfall animation as used on the Tesco homepage for Christmas 2015.

- (incept: )

CSS Snow animation

Updated for Christmas 2019 over on Codepen

A pure CSS solution for the festive season as used for Tesco Christmas 2015, the Tesco homepage, and store-locator too. This piece is a furtherance to Estelle Weyl's snow demo.

One issue outstanding in IE10, where only some flakes spin and drift, which is odd to say the least.


This is what a single row looks like:

Language HTML
<div id=flakes class=flakes>

  <!-- 1 row at 768px -->

  <!-- Common device widths -->
  <i class=snow-1024px></i><i class=snow-1024px></i><i class=snow-1024px></i>
  <i class=snow-1280px></i><i class=snow-1280px></i>
  <i class=snow-1366px></i><i class=snow-1366px></i>
  <i class=snow-1440px></i>
  <i class=snow-1600px></i><i class=snow-1600px></i>
  <i class=snow-1800px></i><i class=snow-1800px></i>
  <i class=snow-1920px></i><i class=snow-1920px></i>


To increase the number of flakes simply duplicate ALL <i> tags and increase .flakes offset height, and falling distance.

Disable flex while changing the quantities of snowflakes. Otherwise flex will resize the <i> tags to fit the available width.


For the sake of brevity I've excluded browser specific prefixes here. Though the stand-alone demo contains everything you need.

The snowflakes container

flex is used to space the <i> tags evenly across the horizontal plane. Playing nicely at in-between device widths. Be sure to temporarily disable flex if you are adjusting the number of flakes. 20vh is allocated per row of flakes. The falling animation would require adjustment too.

Hardware acceleration is encouraged via 3D transforms.

Language CSS
.flakes {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  text-align: center;
  z-index: -1;

  transform: translate(0, -20vh);
  transform: translate3d(0, -20vh, 0);

  justify-content: space-between;
  flex-wrap: wrap;

Cover the flakes container with the background color. That's in case low screen height allows the start position of the flakes to show through.

Language CSS
.flakes div {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: #fff;

Generic styling of the snowflakes, setting defaults for the animation and transforms. Again encouraging hardware acceleration where supported.

Language CSS
.flakes i {
  display: inline-block;

  transform: translate3d(0, 0, 0);
  backface-visibility: hidden;
  perspective: 1000;
  animation: falling;
  animation-iteration-count: infinite;

  background-size: contain;
  background-repeat: no-repeat;
  backface-visibility: hidden;
  /* Updated to SVG */
  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='' viewBox='0 0 96 96' fill='%23fff' fill-rule='evenodd'%3E%3Cpath d='M38.2 65l1-9.7 6.8-3.8v7.8L38.2 65zM58 65l-8-5.8v-7.8l6.8 4 1 9.6zm9.8-17l-9 3.8-6.6-3.8 7-4 8.6 4zm-39.4 0l8.7-4 7 4-6.5 3.8-8.8-4zM58 31l-1 9.6-7 4v-8l8-5.7zm-19.8 0l7.8 5.5v8l-6.7-3.8-1-9.8zm12-17L60.5 3.5l2.8 3-13.3 13v12L63 22l-1.7 16 10.2-5.7 5-18.3 4 1-4 14.4 12.2-7 2 3.6-12 7L93 37l-1.3 3.8-18.2-5-10.2 5.8L78 48l-14.7 6.4 10 5.6 18-5 1.2 4-14.3 3.8L90.6 70l-2 3.5L76 66.3l4 14.4-4 1-5-18.2-9.8-5.6L63 73.3 50 64v11.6L63.3 89l-2.8 3L50 81.5V96h-4V81.5L35.4 92l-2.8-3L46 75.8V64.4l-12.8 9.3 1.7-16-10.5 6-5 18-4-1 4-14.3-12.5 7L5 70l13-7.2-14.5-4 1-3.8L23 60l10-5.7L18.3 48l14.4-6.4-10.2-6-18.3 5-1-3.8 14.3-4-12.2-7 2-3.4 12.2 7-4-14.4 4-1 5 18.2 10.4 6-2-16 13 9.2V19.8L32.4 6.3l3-2.8L46 14V0h4v14z'/%3E%3C/svg%3E");

There are four flake sizes, each requires an individual origin set.

Language CSS
.flakes i:nth-child(4n+0) {
  width: 130px;
  height: 130px;
  transform-origin: -5% -5%;
.flakes i:nth-child(4n+1) {
  width: 97px;
  height: 97px;
  transform-origin: 15% 0;
.flakes i:nth-child(4n+2) {
  width: 65px;
  height: 65px;
  transform-origin: 0 -30%;
.flakes i:nth-child(4n+3) {
  width: 32px;
  height: 32px;
  transform-origin: -100% -100%;

Keep the first snowflake close to the left edge:

Language CSS
.flakes i:first-child {
  transform-origin: 60% 40%;

Delay the start times of the snowflake animations:

Language CSS
.flakes i:nth-of-type(5n+0) {
  animation-delay: 0s;
.flakes i:nth-of-type(5n+1) {
  animation-delay: 2s;
.flakes i:nth-of-type(5n+2) {
  animation-delay: 4s;
.flakes i:nth-of-type(5n+3) {
  animation-delay: 6s;
.flakes i:nth-of-type(5n+4) {
  animation-delay: 8s;

Animation durations are calculated to be multiples of each other +/- a few seconds for initial delays.

Language CSS
.flakes i:nth-child(3n+0) {
  animation-duration: 12s;
.flakes i:nth-child(3n+1) {
  animation-duration: 18s;
.flakes i:nth-child(3n+2) {
  animation-duration: 24s;

Tweak the timing functions to improve the randomness.

Language CSS
.flakes i:nth-of-type(6n+0) {
  animation-timing-function: ease-in-out;
.flakes i:nth-of-type(6n+1) {
  animation-timing-function: ease-out;
.flakes i:nth-of-type(6n+2) {
  animation-timing-function: ease;
.flakes i:nth-of-type(6n+3) {
  animation-timing-function: ease-in;
.flakes i:nth-of-type(6n+4) {
  animation-timing-function: linear;
.flakes i:nth-of-type(6n+5) {
  animation-timing-function: cubic-bezier(0.2, 0.3, 0.8, 0.9);

Tweak opacity for even more randomness.

Language CSS
.flakes i:nth-of-type(7n+0) {opacity: 0.5;}
.flakes i:nth-of-type(7n+1) {opacity: 0.8;}
.flakes i:nth-of-type(7n+2) {opacity: 0.3;}
.flakes i:nth-of-type(7n+4) {opacity: 0.7;}
.flakes i:nth-of-type(7n+6) {opacity: 0.6;}

Increase the number of snowflakes at common device widths. Remember that flex evenly distributes them across the horizontal.

Language CSS
.flakes .snow-1024px,
.flakes .snow-1280px,
.flakes .snow-1366px,
.flakes .snow-1440px,
.flakes .snow-1600px,
.flakes .snow-1800px,
.flakes .snow-1920px {
  display: none
@media screen and (max-width:  767px) {
  .flakes {display: none}
@media screen and (min-width: 1024px) {
  .flakes .snow-1024px {display: inline-block}
@media screen and (min-width: 1280px) {
  .flakes .snow-1280px {display: inline-block}
@media screen and (min-width: 1366px) {
  .flakes .snow-1366px {display: inline-block}
@media screen and (min-width: 1440px) {
  .flakes .snow-1440px {display: inline-block}
@media screen and (min-width: 1600px) {
  .flakes .snow-1600px {display: inline-block}
@media screen and (min-width: 1800px) {
  .flakes .snow-1800px {display: inline-block}
@media screen and (min-width: 1920px) {
  .flakes .snow-1920px {display: inline-block}

One simple animation function for all the snowflakes:

Language CSS
@keyframes falling {
  from {
      translate(0, 0)
  to {
      translate(0, 120vh)

Adding snowflakes dynamically

Need to get around fixed template limitations? JavaScript once again to the rescue, and it may as well late-load all the assets too.

Language JavaScript
(function () {
  "use strict";

  // Dynamically add snowflake HTML & CSS to the page upon page-load.

  function letItSnow() {
    var flakesDiv = document.createElement("div"),
        link = document.createElement("link"),
        html = "";

    // Fetch snow CSS:
    link.href = "snow.css";
    link.rel = "stylesheet";

    // Build HTML: = "flakes";
    flakesDiv.className = "flakes";

    // 1 row at 768px
    html += '<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>';

    // Common device widths
    html += '<i class=snow-1024px></i><i class=snow-1024px></i><i class=snow-1024px></i>';
    html += '<i class=snow-1280px></i><i class=snow-1280px></i>';
    html += '<i class=snow-1366px></i><i class=snow-1366px></i>';
    html += '<i class=snow-1440px></i>';
    html += '<i class=snow-1600px></i><i class=snow-1600px></i>';
    html += '<i class=snow-1800px></i><i class=snow-1800px></i>';
    html += '<i class=snow-1920px></i><i class=snow-1920px></i>';
    html += '<div></div>';

    // Add HTML
    flakesDiv.innerHTML = html;
    document.body.insertBefore(flakesDiv, document.body.childNodes[0]);

  // classList test to eliminate IE9 specifically
  if (window.addEventListener && document.documentElement.classList) {
    window.addEventListener("load", letItSnow, false);