Animated Weather Icons
Six weather icons, each with its own little loop: the sun’s rays turn and breathe, clouds drift, rain falls, snowflakes tumble and lightning flickers out of a dark cloud.
How to use it
- Paste the snippet just before
</head>. - Add
data-animated-weather-iconsto an empty<span>, set to the icon you want:sun,partly,cloud,rain,snoworstorm. - Size it with
font-size: the icon is 1em square.
Options
Add any of these attributes to the same element.
data-speed | Speed up or slow down the loop: 2 is twice as fast. Default 1. |
Each icon gets an aria-label naming the weather; set your own (“Rain, 12°C”) if you like. The loops pause while off screen, and stay still with reduced motion.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Animated Weather Icons: sun, cloud, rain, snow, storm and partly cloudy
// icons, each with its own little loop. The sun's rays turn and breathe,
// clouds drift, rain falls, snowflakes tumble and lightning flickers.
// Usage: <span data-animated-weather-icons="rain"></span>
// Icons: sun, cloud, rain, snow, storm, partly. Size it with font-size
// (the icon is 1em square).
// Optional: data-speed (a multiplier, 2 is twice as fast; default 1).
// The loops pause while off screen, and stay still with reduced motion.
var LABELS = { sun: 'Sunny', cloud: 'Cloudy', rain: 'Rain', snow: 'Snow', storm: 'Thunderstorm', partly: 'Partly cloudy' };
var NS = 'http://www.w3.org/2000/svg';
var css =
'.awi{display:inline-block;width:1em;height:1em;line-height:0;vertical-align:middle}' +
'.awi svg{display:block;width:100%;height:100%;overflow:visible}' +
'.awi svg [class]{transform-box:fill-box;transform-origin:center}' +
'.awi-off *{animation-play-state:paused!important}' +
'.awi-spin{animation:awi-spin 14s linear infinite}' +
'.awi-breathe{animation:awi-breathe 1.8s ease-in-out infinite alternate}' +
'.awi-core{animation:awi-breathe 2.4s ease-in-out infinite alternate}' +
'.awi-drift{animation:awi-drift 3.2s ease-in-out infinite alternate}' +
'.awi-drift2{animation:awi-drift 4.4s ease-in-out infinite alternate-reverse}' +
'.awi-drop{animation:awi-fall .9s linear infinite}' +
'.awi-flake{animation:awi-snow 2.8s linear infinite}' +
'.awi-bolt{animation:awi-bolt 3s linear infinite}' +
'.awi-flash{animation:awi-flash 3s linear infinite}' +
'.awi-cloud{filter:drop-shadow(0 1.5px 2px rgba(30,41,59,.28))}' +
'@keyframes awi-spin{to{transform:rotate(360deg)}}' +
'@keyframes awi-breathe{from{transform:scale(.94)}to{transform:scale(1.06)}}' +
'@keyframes awi-drift{from{transform:translateX(-2px)}to{transform:translateX(2.5px)}}' +
'@keyframes awi-fall{0%{transform:translate(1px,-4px);opacity:0}25%{opacity:1}100%{transform:translate(-3px,12px);opacity:0}}' +
'@keyframes awi-snow{0%{transform:translate(0,-4px) rotate(0);opacity:0}20%{opacity:1}50%{transform:translate(2.5px,4px) rotate(90deg)}80%{opacity:1}100%{transform:translate(-1.5px,13px) rotate(180deg);opacity:0}}' +
'@keyframes awi-bolt{0%,2%{opacity:0}5%{opacity:1}9%{opacity:.25}13%{opacity:1}55%{opacity:1}68%,100%{opacity:0}}' +
'@keyframes awi-flash{0%,2%,20%,100%{filter:brightness(1)}5%,13%{filter:brightness(1.35)}9%{filter:brightness(1.1)}}' +
'@media (prefers-reduced-motion:reduce){.awi svg *{animation:none!important}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// A cloud as a union of circles, spanning roughly x 14–52, y 20–46.
function cloud(fill, cls) {
return '<g class="' + cls + '" fill="' + fill + '"><circle cx="22" cy="38" r="8"/><circle cx="33" cy="31" r="11"/>' +
'<circle cx="44" cy="38" r="8"/><rect x="14" y="36" width="38" height="10" rx="5"/></g>';
}
function sun() {
var rays = '';
for (var i = 0; i < 8; i++) {
var a = i * Math.PI / 4, c = Math.cos(a), s = Math.sin(a);
rays += '<line x1="' + (32 + c * 16).toFixed(2) + '" y1="' + (32 + s * 16).toFixed(2) + '" x2="' + (32 + c * 22).toFixed(2) + '" y2="' + (32 + s * 22).toFixed(2) + '"/>';
}
return '<g class="awi-spin"><g class="awi-breathe" stroke="#f59e0b" stroke-width="3.6" stroke-linecap="round">' +
'<circle cx="32" cy="32" r="22" fill="none" stroke="none"/>' + rays + '</g></g>' +
'<circle class="awi-core" cx="32" cy="32" r="11" fill="#fbbf24"/>';
}
function drops(color, xs) {
return xs.map(function (x, i) {
return '<line class="awi-drop" style="animation-delay:' + -(i * 0.9 / xs.length).toFixed(2) + 's" x1="' + x + '" y1="42" x2="' + (x - 2) + '" y2="48" stroke="' + color + '" stroke-width="3" stroke-linecap="round"/>';
}).join('');
}
function flakes(xs) {
return xs.map(function (x, i) {
return '<g transform="translate(' + x + ' ' + (44 + (i % 2) * 3) + ')"><g class="awi-flake" style="animation-delay:' + -(i * 2.8 / xs.length).toFixed(2) + 's" stroke="#60a5fa" stroke-width="1.7" stroke-linecap="round">' +
'<path d="M0 -3.2 V3.2 M-2.8 -1.6 L2.8 1.6 M-2.8 1.6 L2.8 -1.6"/></g></g>';
}).join('');
}
var ICONS = {
sun: sun,
cloud: function () {
return '<g transform="translate(9 -7) scale(.8)">' + cloud('#a8b6cb', 'awi-cloud awi-drift2') + '</g>' +
'<g transform="translate(-2 4)">' + cloud('#fff', 'awi-cloud awi-drift') + '</g>';
},
rain: function () {
return drops('#3b82f6', [21, 31, 41, 26, 36, 46]) +
'<g transform="translate(0 -9)">' + cloud('#cbd5e1', 'awi-cloud awi-drift') + '</g>';
},
snow: function () {
return flakes([20, 28, 36, 44, 24, 40]) +
'<g transform="translate(0 -9)">' + cloud('#e2e8f0', 'awi-cloud awi-drift') + '</g>';
},
storm: function () {
return drops('#60a5fa', [19, 45, 24, 40]) +
'<path class="awi-bolt" d="M35 36 L26 50 H32.5 L29 61 L40 46 H33.5 L37 36 Z" fill="#facc15" stroke="#eab308" stroke-width="1" stroke-linejoin="round"/>' +
'<g transform="translate(0 -9)"><g class="awi-flash">' + cloud('#64748b', 'awi-cloud awi-drift') + '</g></g>';
},
partly: function () {
return '<g transform="translate(-6 -6) scale(.78)">' + sun() + '</g>' +
'<g transform="translate(4 6)">' + cloud('#fff', 'awi-cloud awi-drift') + '</g>';
}
};
function setup(el) {
var name = el.getAttribute('data-animated-weather-icons') || 'sun';
if (!ICONS[name]) name = 'sun';
el.classList.add('awi', 'awi-off');
var svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 64 64');
svg.setAttribute('aria-hidden', 'true');
svg.innerHTML = ICONS[name]();
el.appendChild(svg);
var speed = +el.dataset.speed;
if (speed > 0) {
Array.prototype.forEach.call(svg.querySelectorAll('[class]'), function (n) {
var cs = getComputedStyle(n);
var dur = parseFloat(cs.animationDuration);
if (!dur) return;
n.style.animationDuration = dur / speed + 's';
n.style.animationDelay = parseFloat(cs.animationDelay) / speed + 's';
});
}
if (!el.getAttribute('aria-label') && !el.closest('[aria-hidden="true"]')) {
el.setAttribute('role', 'img');
el.setAttribute('aria-label', LABELS[name]);
}
}
function init() {
// Loops only run while the icon is on screen.
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) { e.target.classList.toggle('awi-off', !e.isIntersecting); });
});
document.querySelectorAll('[data-animated-weather-icons]').forEach(function (el) {
if (el._awi) return;
el._awi = true;
setup(el);
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><span data-animated-weather-icons="rain" style="font-size: 64px"></span>
<span data-animated-weather-icons="sun" aria-label="Sunny, 24°C" style="font-size: 64px"></span>