Screen Edge Glow
A soft, colourful glow runs round the edges of the screen while an assistant listens. The colours chase each other round the border and the glow breathes with the voice. It blooms up from the bottom edge when listening starts and fades away when it stops.
Listening…
How to use it
- Paste the snippet just before
</head>. - Add
data-screen-edge-glowto a panel. The glow follows its rounded corners and stays inside it. - Or add
data-screenas well to glow round the whole browser window.
Options
Add any of these attributes to the same element.
data-screen | Glow round the edges of the whole window instead of the element. |
data-colors | Comma-separated colours. Default #8b5cf6,#ec4899,#f59e0b,#22d3ee. |
data-width | Glow thickness. Default 14 px. |
data-speed | Seconds for the colours to go once round. Default 5. |
data-autoplay | Start and stop listening by itself, every few seconds. |
data-off | Start switched off. |
Switch it from your code with animacious.screenEdgeGlow(el, true) when listening starts and false when it stops. The glow is drawn with CSS only (a turning conic gradient), so it’s light on the battery.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Screen Edge Glow: a soft, colourful glow runs round the edges of a panel
// (or the whole screen) while an assistant is listening. The colours chase
// each other round the border, and the glow breathes with a pretend voice.
// It blooms in from the bottom edge when listening starts and fades away
// when it stops.
// Usage: <div data-screen-edge-glow>…</div> (follows the element's
// rounded corners, and clips its overflow so the glow stays inside)
// Optional: data-screen (glow round the whole browser window instead),
// data-colors (comma-separated, default "#8b5cf6,#ec4899,#f59e0b,#22d3ee"),
// data-width (glow thickness in px, default 14), data-speed (seconds per lap,
// default 5), data-autoplay (start and stop listening by itself, every few
// seconds), data-off (start switched off).
// Script use: animacious.screenEdgeGlow(el, true | false) switches it on or off.
var DEFAULTS = { colors: '#8b5cf6,#ec4899,#f59e0b,#22d3ee', width: 14, speed: 5 };
var css =
'@property --seg-a{syntax:"<angle>";inherits:true;initial-value:0deg}' +
'[data-screen-edge-glow]{position:relative}' +
'[data-screen-edge-glow]:not([data-screen]){overflow:hidden}' +
'.seg-layer{position:absolute;inset:0;border-radius:inherit;pointer-events:none;z-index:3;' +
'--seg-l:0;animation:seg-spin var(--seg-speed,5s) linear infinite;opacity:0;' +
'transform-origin:50% 100%;transition:opacity .7s ease,transform .9s cubic-bezier(.2,.8,.2,1);transform:scaleY(.2)}' +
'.seg-layer.seg-fixed{position:fixed;z-index:2147483000;border-radius:0}' +
'.seg-on{opacity:1;transform:none}' +
'.seg-ring,.seg-glow{position:absolute;inset:0;border-radius:inherit;' +
'background:conic-gradient(from var(--seg-a),var(--seg-stops));' +
'-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;' +
'mask:linear-gradient(#000 0 0) content-box exclude,linear-gradient(#000 0 0)}' +
'.seg-ring{padding:calc(var(--seg-w) * .22)}' +
'.seg-blur{position:absolute;inset:0;border-radius:inherit;filter:blur(calc(var(--seg-w) * .7));opacity:calc(.6 + var(--seg-l) * .4)}' +
'.seg-glow{padding:calc(var(--seg-w) * (1 + var(--seg-l) * .7))}' +
'@keyframes seg-spin{to{--seg-a:360deg}}' +
'@media (prefers-reduced-motion: reduce){.seg-layer{animation:none;transition:opacity .3s;transform:none}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function voice() {
var target = 0, until = 0;
return function (t) {
if (t > until) {
var r = Math.random();
if (r < 0.15) { target = 0; until = t + 400 + Math.random() * 500; }
else { target = 0.3 + Math.random() * 0.7; until = t + 90 + Math.random() * 200; }
}
return target;
};
}
function setup(el) {
var d = el.dataset;
var cols = (d.colors || DEFAULTS.colors).split(',').map(function (c) { return c.trim(); });
// Repeat the first colour at the end so the conic gradient has no seam.
var stops = cols.concat(cols[0]).join(',');
var layer = document.createElement('div');
layer.className = 'seg-layer' + (d.screen !== undefined ? ' seg-fixed' : '');
layer.setAttribute('aria-hidden', 'true');
layer.style.setProperty('--seg-stops', stops);
layer.style.setProperty('--seg-w', (+d.width || DEFAULTS.width) + 'px');
layer.style.setProperty('--seg-speed', (+d.speed || DEFAULTS.speed) + 's');
layer.innerHTML = '<div class="seg-blur"><div class="seg-glow"></div></div><div class="seg-ring"></div>';
el.appendChild(layer);
var st = { layer: layer, on: false, raf: 0, visible: false, timer: null, level: 0, fake: voice() };
el._seg = st;
function breathe(now) {
st.raf = 0;
if (!st.visible || !st.on) return;
st.level += (st.fake(now) - st.level) * 0.12;
layer.style.setProperty('--seg-l', st.level.toFixed(3));
st.raf = requestAnimationFrame(breathe);
}
st.kick = function () { if (!st.raf && !reduced() && st.visible && st.on) st.raf = requestAnimationFrame(breathe); };
function cycle() {
toggle(el, !st.on);
st.timer = setTimeout(cycle, st.on ? 5200 : 1800);
}
new IntersectionObserver(function (entries) {
st.visible = entries[0].isIntersecting;
if (!st.visible) return;
if (!st.started) {
st.started = true;
if (d.off === undefined) toggle(el, true);
if (d.autoplay !== undefined && d.autoplay !== 'false' && !reduced()) st.timer = setTimeout(cycle, 5200);
}
st.kick();
}, { threshold: 0.2 }).observe(d.screen !== undefined ? document.documentElement : el);
}
function toggle(el, on) {
var st = el._seg;
st.on = !!on;
st.layer.classList.toggle('seg-on', st.on);
if (st.on) st.kick();
}
function init() {
document.querySelectorAll('[data-screen-edge-glow]').forEach(function (el) {
if (el._seg) return;
setup(el);
});
}
window.animacious = window.animacious || {};
window.animacious.screenEdgeGlow = function (el, on) {
if (!el._seg) setup(el);
clearTimeout(el._seg.timer);
toggle(el, on);
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-screen-edge-glow style="border-radius: 24px">…</div>
<!-- round the whole window, off until you switch it on: -->
<body data-screen-edge-glow data-screen data-off>…</body>