Battery Level
A battery fills to its percentage while the reading counts up beside it. A charging bolt pops in and pulses, a soft shine runs through the charge, and the fill turns from red to amber to green as it climbs.
How to use it
- Paste the snippet just before
</head>. - Add
data-battery-level="76"(the percentage) to an empty element. Size it withfont-size; the battery takes the text colour.
Options
Add any of these attributes to the same element.
data-from | Where the fill starts. Default 0. |
data-plain | No bolt or shine: just the level, not charging. |
data-loop | Drain back and charge again every few seconds. |
data-hold | How long it rests full before looping. Default 2200 ms. |
data-duration | How long a full charge takes. Default 1800 ms. |
data-colors | Low (to 20%), middle (to 50%) and healthy colours. Default #ef4444,#f59e0b,#22c55e. |
data-hide-value | No percentage beside the battery. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Battery Level: a battery fills to its percentage while the reading counts
// up beside it. A charging bolt pulses in the middle and a soft shine runs
// through the charge. The fill turns red when low, amber in the middle and
// green when healthy.
// Usage: <div data-battery-level="76"></div> (percent)
// Optional: data-from (where the fill starts, default 0), data-plain (no
// bolt or shine: just the level), data-loop (drain back and charge again
// every few seconds), data-hold (ms it rests full before looping, default
// 2200), data-duration (ms to fill, default 1800), data-colors (low, middle
// and healthy colours, default "#ef4444,#f59e0b,#22c55e"), data-hide-value
// (no reading beside it).
// Size it with font-size: the battery is 5em wide.
var DEFAULTS = { from: 0, hold: 2200, duration: 1800, colors: '#ef4444,#f59e0b,#22c55e' };
var css =
'.btl{display:inline-flex;align-items:center;gap:.6em;font-variant-numeric:tabular-nums}' +
'.btl-cell{position:relative;flex:none;width:5em;height:2.5em;margin-right:.32em;border:.2em solid currentColor;border-radius:.62em;padding:.2em;box-sizing:border-box}' +
'.btl-cell::after{content:"";position:absolute;right:-.5em;top:50%;width:.3em;height:.9em;margin-top:-.45em;border-radius:0 .2em .2em 0;background:currentColor}' +
'.btl-fill{display:block;position:relative;height:100%;width:0;border-radius:.34em;overflow:hidden;transition:background-color .4s}' +
// A shine that sweeps through the charge while charging.
'.btl-fill::after{content:"";position:absolute;inset:0;width:60%;transform:translateX(var(--btl-x,-100%));' +
'background:linear-gradient(90deg,transparent,rgba(255,255,255,.55),transparent);opacity:var(--btl-shine,0)}' +
'.btl-bolt{position:absolute;left:50%;top:50%;width:1.3em;height:1.6em;margin:-.8em 0 0 -.65em;fill:#fff;stroke:currentColor;stroke-width:1.6;' +
'stroke-linejoin:round;paint-order:stroke;filter:drop-shadow(0 .05em .1em rgba(0,0,0,.25))}' +
'.btl-num{font-size:1.5em;font-weight:800;letter-spacing:-.03em;min-width:2.8em;text-align:left}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
var b = {
el: el, level: Math.max(0, Math.min(100, parseFloat(el.getAttribute('data-battery-level')) || 0)),
from: d.from !== undefined ? +d.from : DEFAULTS.from, hold: +d.hold || DEFAULTS.hold,
duration: +d.duration || DEFAULTS.duration, colors: (d.colors || DEFAULTS.colors).split(','),
plain: d.plain !== undefined, loop: d.loop !== undefined && d.loop !== 'false',
still: reduced(), visible: false, started: false, timer: 0, anims: [], value: 0
};
el.classList.add('btl');
el.innerHTML = '<span class="btl-cell" aria-hidden="true"><span class="btl-fill"></span>' +
(b.plain ? '' : '<svg class="btl-bolt" viewBox="0 0 16 20"><path d="M9.5 1L2 11.5h5.2L6 19l8-10.6H8.7L9.5 1z"/></svg>') +
'</span>' + (d.hideValue !== undefined ? '' : '<span class="btl-num" aria-hidden="true"></span>');
b.fill = el.querySelector('.btl-fill');
b.bolt = el.querySelector('.btl-bolt');
b.num = el.querySelector('.btl-num');
el.setAttribute('role', 'img');
el.setAttribute('aria-label', 'Battery ' + Math.round(b.level) + '%' + (b.plain ? '' : ', charging'));
if (b.bolt) b.bolt.style.opacity = 0;
draw(b, b.still ? b.level : b.from);
if (b.still && b.bolt) b.bolt.style.opacity = 1;
return b;
}
function colour(b, v) { return v <= 20 ? b.colors[0] : v <= 50 ? b.colors[1] || b.colors[0] : b.colors[2] || b.colors[1]; }
function draw(b, v) {
b.value = v;
b.fill.style.width = v + '%';
b.fill.style.backgroundColor = colour(b, v);
if (b.num) b.num.textContent = Math.round(v) + '%';
}
function stop(b) {
clearTimeout(b.timer);
b.anims.forEach(function (a) { a.pause(); });
b.anims = [];
}
function charge(b) {
stop(b);
if (!b.visible) return;
var st = { v: b.value };
b.anims.push(anime.animate(st, {
v: b.level, duration: b.duration * Math.max(0.35, Math.abs(b.level - b.value) / 100), ease: 'inOut(2)',
onUpdate: function () { draw(b, st.v); },
onComplete: function () { if (b.loop) b.timer = setTimeout(function () { drain(b); }, b.hold); }
}));
if (!b.plain) {
// Bolt pops in and keeps pulsing; a shine runs through the charge.
b.anims.push(anime.animate(b.bolt, { opacity: [0, 1], scale: [0.4, 1], duration: 500, ease: 'outBack(2)' }));
b.anims.push(anime.animate(b.bolt, { scale: [1, 1.14, 1], duration: 1200, delay: 500, loop: true, ease: 'inOut(2)' }));
var shine = { x: -100 };
b.fill.style.setProperty('--btl-shine', 1);
b.anims.push(anime.animate(shine, {
x: 260, duration: 1500, loop: true, ease: 'inOut(2)', loopDelay: 500,
onUpdate: function () { b.fill.style.setProperty('--btl-x', shine.x + '%'); }
}));
}
}
function drain(b) {
stop(b);
if (!b.visible) return;
var st = { v: b.value };
if (b.bolt) b.anims.push(anime.animate(b.bolt, { opacity: 0, scale: 0.4, duration: 300, ease: 'in(2)' }));
b.fill.style.setProperty('--btl-shine', 0);
b.anims.push(anime.animate(st, {
v: b.from, duration: 900, ease: 'inOut(2)', onUpdate: function () { draw(b, st.v); },
onComplete: function () { b.timer = setTimeout(function () { charge(b); }, 500); }
}));
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var b = e.target._btl;
b.visible = e.isIntersecting;
if (b.visible) {
charge(b); // fills (or carries on filling) and restarts the bolt pulse and shine
b.started = true;
} else stop(b);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-battery-level]').forEach(function (el) {
if (el._btl) return;
var b = (el._btl = setup(el));
if (!b.still) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-battery-level="76" style="font-size: 24px"></div>
<!-- a low battery, not charging: -->
<div data-battery-level="12" data-plain></div>