Breathing Glow
A soft glow swells around the text and fades away again, slow and even, like someone breathing in their sleep. The letters swell a touch as it breathes in.
Breathe in
How to use it
- Paste the snippet just before
</head>. - Add
data-breathing-glowto any heading or text element. It glows brightest on a dark background.
Options
Add any of these attributes to the same element.
data-color | Colour of the glow. Default #a855f7. |
data-duration | One whole breath, in and out. Default 5000 ms. |
data-size | How far the glow spreads at its fullest, in em. Default 0.6. |
data-scale | How much the text swells as it breathes in. Default 1.03; 1 for none. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Breathing Glow: a soft glow swells around the text and fades away again,
// slow and even like someone breathing in and out while they sleep.
// Usage: <h1 data-breathing-glow>Breathe in</h1>
// Optional: data-color (colour of the glow; default #a855f7), data-duration
// (one whole breath, in and out, in ms; default 5000), data-size (how far
// the glow spreads at its fullest, in em; default 0.6), data-scale (how
// much the text swells as it breathes in; default 1.03, 1 for none).
var DEFAULTS = { color: '#a855f7', duration: 5000, size: 0.6, scale: 1.03 };
var css = '.bgl-on{display:inline-block;will-change:transform,text-shadow}';
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 color = d.color || DEFAULTS.color;
var duration = +d.duration || DEFAULTS.duration;
var size = d.size !== undefined ? +d.size : DEFAULTS.size;
var scale = d.scale !== undefined ? +d.scale : DEFAULTS.scale;
// The glow sits on an inner span so the element keeps its own display.
var inner = document.createElement('span');
inner.className = 'bgl-on';
while (el.firstChild) inner.appendChild(el.firstChild);
el.appendChild(inner);
function c(alpha) { return 'color-mix(in srgb, ' + color + ' ' + Math.round(alpha * 100) + '%, transparent)'; }
// b runs from 0 (breathed out) to 1 (lungs full).
function paint(b) {
var s = size * (0.25 + 0.75 * b);
inner.style.textShadow =
'0 0 ' + (s * 0.12).toFixed(3) + 'em ' + c(0.35 + 0.5 * b) + ',' +
'0 0 ' + (s * 0.45).toFixed(3) + 'em ' + c(0.15 + 0.55 * b) + ',' +
'0 0 ' + s.toFixed(3) + 'em ' + c(0.08 + 0.42 * b);
inner.style.transform = 'scale(' + (1 + (scale - 1) * b).toFixed(4) + ')';
}
if (reduced()) { paint(0.7); return; }
var breath = { b: 0 };
paint(0);
var tl = anime.createTimeline({ loop: true, autoplay: false, onUpdate: function () { paint(breath.b); } })
// Breathe in, a beat at the top, a longer breath out, a pause.
.add(breath, { b: 1, duration: duration * 0.4, ease: 'inOutSine' })
.add(breath, { b: 1, duration: duration * 0.08 })
.add(breath, { b: 0, duration: duration * 0.44, ease: 'inOutSine' })
.add(breath, { b: 0, duration: duration * 0.08 });
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) tl.play();
else tl.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-breathing-glow]').forEach(function (el) {
if (el._bgl) return;
el._bgl = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-breathing-glow>Breathe in</h1>
<!-- a slow, wide, teal glow: -->
<h1 data-breathing-glow data-color="#2dd4bf" data-duration="8000" data-size="0.9">Relax</h1>