Radar Sweep
A beam circles a little radar screen, dragging a fading trail behind it. Blips light up as the beam passes over them and fade away before it comes round again, and now and then one moves somewhere new. Size it with font-size and colour it with color.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-radar-sweep>where the loader should appear.
Options
Add any of these attributes to the same element.
data-speed | Time for one turn of the beam. Default 2400 ms. |
data-blips | Number of blips, 0 to 8. Default 3. |
data-trail | Length of the trail behind the beam, in degrees, 20 to 270. Default 100. |
data-blip-color | Colour of the blips. Default color. |
data-grid="false" | Hide the rings and cross hairs. |
It’s 3em across. It only moves while it’s on screen. With reduced motion the beam and blips sit still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Radar Sweep: a beam circles a radar screen, leaving a fading trail, and
// blips light up as it passes over them, then fade until the next sweep.
// Now and then a blip moves somewhere new. Sized with font-size (3em
// across), coloured with color. It only moves while it's on screen.
// Usage: <div data-radar-sweep aria-label="Searching"></div>
// Optional: data-speed (ms for one turn, default 2400), data-blips (0 to 8,
// default 3), data-trail (length of the trail in degrees, 20 to 270,
// default 100), data-grid="false" (no rings or cross), data-blip-color
// (blip colour, default the same as color).
var DEFAULTS = { speed: 2400, blips: 3, trail: 100 };
var SIZE = 3; // em
var css =
'.rds{position:relative;display:inline-block;width:' + SIZE + 'em;height:' + SIZE + 'em;vertical-align:middle;border-radius:50%;overflow:hidden}' +
'.rds>span{position:absolute;pointer-events:none}' +
'.rds-bg{inset:0;border-radius:50%;background:currentColor;opacity:.08}' +
'.rds-edge{inset:0;border-radius:50%;border:.06em solid currentColor;opacity:.55}' +
'.rds-ring{border-radius:50%;border:.03em solid currentColor;opacity:.28}' +
'.rds-ring1{inset:16.66%}.rds-ring2{inset:33.33%}' +
'.rds-x{left:50%;top:4%;bottom:4%;width:.03em;margin-left:-.015em;background:currentColor;opacity:.22}' +
'.rds-y{top:50%;left:4%;right:4%;height:.03em;margin-top:-.015em;background:currentColor;opacity:.22}' +
'.rds-trail{inset:0;border-radius:50%;will-change:transform}' +
'.rds-beam{left:50%;bottom:50%;width:.06em;height:50%;margin-left:-.03em;border-radius:.03em;background:currentColor;transform-origin:50% 100%;will-change:transform;box-shadow:0 0 .12em currentColor}' +
'.rds-hub{left:50%;top:50%;width:.16em;height:.16em;margin:-.08em 0 0 -.08em;border-radius:50%;background:currentColor}' +
'.rds-blip{left:50%;top:50%;width:.2em;height:.2em;margin:-.1em 0 0 -.1em;border-radius:50%;background:currentColor;box-shadow:0 0 .2em .04em currentColor;opacity:0;will-change:transform,opacity}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function span(el, cls) {
var s = document.createElement('span');
s.className = cls;
s.setAttribute('aria-hidden', 'true');
el.appendChild(s);
return s;
}
// A blip somewhere inside the screen, away from the very middle.
function place(b) {
b.angle = Math.random() * 360;
b.r = 0.25 + Math.random() * 0.6; // fraction of the radius
var a = b.angle * Math.PI / 180, r = b.r * SIZE / 2;
b.el.style.transform = 'translate(' + (Math.sin(a) * r).toFixed(3) + 'em,' + (-Math.cos(a) * r).toFixed(3) + 'em)';
}
function build(el) {
var d = el.dataset;
var n = Math.max(0, Math.min(8, d.blips != null && !isNaN(+d.blips) ? Math.round(+d.blips) : DEFAULTS.blips));
var trail = Math.max(20, Math.min(270, +d.trail || DEFAULTS.trail));
el.classList.add('rds');
if (!el.getAttribute('role')) el.setAttribute('role', 'progressbar');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Loading');
el.textContent = '';
span(el, 'rds-bg');
if (d.grid !== 'false') {
span(el, 'rds-ring rds-ring1');
span(el, 'rds-ring rds-ring2');
span(el, 'rds-x');
span(el, 'rds-y');
}
// The trail is a cone that fades in towards the beam. Its leading edge
// sits at 0deg (straight up) and the whole thing turns with the beam.
var tr = span(el, 'rds-trail');
tr.style.background = 'conic-gradient(from ' + (-trail) + 'deg, transparent 0deg, currentColor ' + trail + 'deg, transparent ' + trail + 'deg)';
tr.style.opacity = '.38';
var blips = [];
for (var i = 0; i < n; i++) {
var b = { el: span(el, 'rds-blip'), last: 1 };
if (d.blipColor) b.el.style.color = d.blipColor;
place(b);
blips.push(b);
}
var beam = span(el, 'rds-beam');
span(el, 'rds-hub');
span(el, 'rds-edge');
function draw(deg) {
var rot = 'rotate(' + deg.toFixed(2) + 'deg)';
tr.style.transform = rot;
beam.style.transform = rot;
blips.forEach(function (b) {
// How far round the beam has gone since it last passed this blip.
var age = ((deg - b.angle) % 360 + 360) % 360 / 360;
if (age < b.last && Math.random() < 0.35) {
// The beam has just reached it: sometimes it has moved on.
place(b);
age = ((deg - b.angle) % 360 + 360) % 360 / 360;
}
b.last = age;
var o = Math.pow(1 - age, 2.2);
b.el.style.opacity = o.toFixed(3);
b.el.style.scale = (0.6 + 0.6 * o).toFixed(3);
});
}
draw(60);
if (reduced()) return null;
var speed = Math.max(400, +d.speed || DEFAULTS.speed);
var t = speed / 6, last = 0;
return anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
t += last ? Math.min(50, now - last) : 16;
last = now;
draw(t / speed * 360 % 360);
},
onPause: function () { last = 0; },
});
}
function init() {
document.querySelectorAll('[data-radar-sweep]').forEach(function (el) {
if (el._rds) return;
el._rds = true;
var timer = build(el);
if (!timer) return;
new IntersectionObserver(function (entries) {
entries[0].isIntersecting ? timer.play() : timer.pause();
}).observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-radar-sweep style="font-size: 24px; color: #059669" aria-label="Searching"></div>