
Iām pretty happy with the look and feel of my website but, looking around the indieweb, I see so many creative and fun websites with neat animations and interactive features. Reader, I was jealous; I wanted my website to be more fun. The flying toasters just werenāt enough anymore. My first thought was to add a midi-player. I spent a few hours in a hyperfocus-hole digging up all sorts of fun midi tracks, from Kate Bush to Rammstein. I was excited. But reality hit me like a truck when I learned that HTML5 dropped support for midi files. This meant that it was either going to be a monumental pain in the arse to implement my midi payer, or I was going to have ā¦

Iām pretty happy with the look and feel of my website but, looking around the indieweb, I see so many creative and fun websites with neat animations and interactive features. Reader, I was jealous; I wanted my website to be more fun. The flying toasters just werenāt enough anymore. My first thought was to add a midi-player. I spent a few hours in a hyperfocus-hole digging up all sorts of fun midi tracks, from Kate Bush to Rammstein. I was excited. But reality hit me like a truck when I learned that HTML5 dropped support for midi files. This meant that it was either going to be a monumental pain in the arse to implement my midi payer, or I was going to have to rely on some pretty heavy dependencies. And look, I know typing npm install blah doesnāt seem like a big deal to some folk but, where I can, I would really rather avoid summoning from the ether giant directories full of code that I donāt understand for my little website. To add to that, provided you donāt want your midi files played by some dead-simple synth sound, thereās the business of soundfonts: gigabytes of audio samples from mysterious origins which you have to host yourself if you donāt want Googleās servers tracking all of your visitors. At least one popular soundfont also seems to be a bit of a mystery; where does SGM Plus come from? No one seems to know. How is it licensed? I sure couldnāt find an answer.
So, yea, I gave up on that idea and decided to implement a falling-snow effect instead. Hereās how I did it.
Humble beginnings #
I set out to look for an implementation with as little JavaScript as possible. I have nothing against JavaScript, but I figure itās best to try trimming your toenails with clippers before reaching for a chainsaw. The search led me to a codepen with this HTML and CSS-only solution. I tidied up the formatting, stripped out anything unnecessary, and put together my include, _includes/weather.njk:
<!-- weather -->
<!-- Based on https://codepen.io/codeconvey/pen/xRzQay -->
{# This include causes a symbol (text, emoji, et cetera; from metadata.weatherSymbol) to fall from the top of the viewport like snow. #}
<style>
.fallingObject {
color: #fff;
font-size: 1em;
font-family: Arial;
pointer-events: none;
text-shadow: 0 0 1px #000;
}
@keyframes fallingObjects-fall {
0% {
top: -10%;
}
100% {
top: 100%;
}
}
@keyframes fallingObjects-shake {
0% {
transform: translateX(0px);
}
50% {
transform: translateX(80px);
}
100% {
transform: translateX(0px);
}
}
.fallingObject {
position: fixed;
top: -10%;
z-index: 9999;
user-select: none;
cursor: default;
animation-name: fallingObjects-fall, fallingObjects-shake;
animation-duration: 10s, 3s;
animation-timing-function: linear, ease-in-out;
animation-iteration-count: infinite, infinite;
animation-play-state: running, running;
}
.fallingObject:nth-of-type(0) {
left: 1%;
animation-delay: 0s, 0s;
& > div {
transform: rotate(45deg);
}
}
.fallingObject:nth-of-type(1) {
left: 10%;
animation-delay: 1s, 1s;
& > div {
transform: rotate(10deg);
}
}
.fallingObject:nth-of-type(2) {
left: 20%;
animation-delay: 6s, 0.5s;
& > div {
transform: rotate(60deg);
}
}
.fallingObject:nth-of-type(3) {
left: 30%;
animation-delay: 4s, 2s;
& > div {
transform: rotate(84deg);
}
}
.fallingObject:nth-of-type(4) {
left: 40%;
animation-delay: 2s, 2s;
& > div {
transform: rotate(267deg);
}
}
.fallingObject:nth-of-type(5) {
left: 50%;
animation-delay: 8s, 3s;
& > div {
transform: rotate(200deg);
}
}
.fallingObject:nth-of-type(6) {
left: 60%;
animation-delay: 6s, 2s;
& > div {
transform: rotate(20deg);
}
}
.fallingObject:nth-of-type(7) {
left: 70%;
animation-delay: 2.5s, 1s;
& > div {
transform: rotate(78deg);
}
}
.fallingObject:nth-of-type(8) {
left: 80%;
animation-delay: 1s, 0s;
& > div {
transform: rotate(3120deg);
}
}
.fallingObject:nth-of-type(9) {
left: 90%;
animation-delay: 3s, 1.5s;
& > div {
transform: rotate(123deg);
}
}
</style>
<div class="fallingObjects" id="weather" aria-hidden="true">
<div class="fallingObject">
<div>{{ metadata.weatherSymbol }}</div>
</div>
<div class="fallingObject">
<div>{{ metadata.weatherSymbol }}</div>
</div>
<div class="fallingObject">
<div>{{ metadata.weatherSymbol }}</div>
</div>
<div class="fallingObject">
<div>{{ metadata.weatherSymbol }}</div>
</div>
<div class="fallingObject">
<div>{{ metadata.weatherSymbol }}</div>
</div>
<div class="fallingObject">
<div>{{ metadata.weatherSymbol }}</div>
</div>
<div class="fallingObject">
<div>{{ metadata.weatherSymbol }}</div>
</div>
<div class="fallingObject">
<div>{{ metadata.weatherSymbol }}</div>
</div>
<div class="fallingObject">
<div>{{ metadata.weatherSymbol }}</div>
</div>
<div class="fallingObject">
<div>{{ metadata.weatherSymbol }}</div>
</div>
</div>
<!-- /weather -->
I added this include in my base layout, after the footer, just before the the closing </body> tag. Beyond cleanup, I made the following changes:
- I replaced the snowflakes in the example with
{{ metadata.weatherSymbol }}so that I can easily change the symbol that falls (thatās right folks, thisāll do more than just snowflakes!). - I added a random amount of rotation to each object.
- I changed the class names from anything snowflake related because Iām a pedant.
Now all we need to do is make sure {{ metadata.weatherSymbol }} exists and we should be cooking with gas. To _data/metadata.js I added: weatherSymbol: "š",; a falling leaf for autumn.
Settings #
Now we have our falling-snow falling leaf effect working but, as with anything fun, there are going to be at least a few crabbit souls who will hate this. For them, letās implement a toggle. First, the toggle itself, in _includes/weatherController.njk:
<form id="weatherController">
<input type="checkbox" id="weatherToggle" checked />
<label for="weatherToggle">Show weather?</label>
</form>
Second, a settings modal to hold the toggle, in _includes/siteSettings.njk (if the feature happens to be turned on at the moment you should be able to scroll down to the bottom of the page to see this in action):
<div id="siteSettingsContainer">
<button onclick="siteSettings.showModal();">Site Settings</button>
<dialog id="siteSettings">
<h2>Site Settings</h2>
{% include "weatherController.njk" %}
<button id="settingsDone" onclick="siteSettings.close();">Done</button>
</dialog>
</div>
Alright, now we just need to pop our site settings include into the site footer and wire everything up together.
Wiring it all up #
Letās first add a quick rule to our CSS:
.hidden {
display: none;
}
Then we can work on our script. Letās add it to the bottom of our weather include, _includes/weather.njk, as itās positioned right before the closing </body> tag.
First, weāll check local storage to see if the user has set a preference before; if so, weāll add/remove the .hidden CSS rule to our weather element and update the weather-controller checkbox accordingly:
<script>
const weather = document.getElementById("weather");
const weatherToggle = document.getElementById("weatherToggle");
const weatherPreference = localStorage.getItem("weather");
// Initial weather preference check on page load
if (weatherPreference == 0) {
weather.classList.add("hidden");
weatherToggle.checked = false;
} else {
weather.classList.remove("hidden");
weatherToggle.checked = true;
};
Then, weāll create an event listener on the checkbox, which will add a value into local storage to save our visitorās preference and add/remove that CSS rule whenever a change to the checkbox value is detected:
// Handle weather setting toggle
weatherToggle.addEventListener('change', function() {
if (this.checked) {
localStorage.setItem("weather", 1);
weather.classList.remove("hidden");
} else {
localStorage.setItem("weather", 0);
weather.classList.add("hidden");
};
});
</script>
Done! #
With that, weāre done! We now have a āfalling-snowā effect that can take any emoji (or arbitrary text) set in _data/metadata.js, and can be toggled on and off by the visitor whose preference is retained in local storage across sessions.