Server Rack Blink
Two racks of servers, switches and disk shelves hum away while their status lights blink. Disk lights flicker in bursts, switch ports chatter, power lights glow steady and every so often a warning light flashes amber.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-server-rack-blink>where the scene should go. It fills the element with an SVG scene.
Options
Add any of these attributes to the same element.
data-color | Colour of the activity lights. Default #22c55e. |
data-accent | Colour of the badge on top of each rack. Default #7c3aed. |
data-racks | 1 or 2 racks. Default 2. |
data-rate | Blink rate multiplier: 2 for a busy day. Default 1. |
It is 8:5 on a transparent background by default. It pauses when off screen. With reduced motion the lights hold still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Server Rack Blink: two racks of servers, switches and disk shelves hum
// away while their status lights blink: activity lights flicker in
// bursts, switch ports chatter, power lights glow steady and now and then
// a warning light flashes amber. Drawn as SVG that fills the element, and
// paused when off screen.
// Usage: <div data-server-rack-blink></div>
// It is 8:5 by default on a transparent background.
// Optional: data-color (colour of the activity lights, default #22c55e),
// data-accent (colour of the rack trim and badges, default #7c3aed),
// data-racks (1 or 2, default 2), data-rate (blink rate multiplier,
// default 1).
var DEFAULTS = { color: '#22c55e', accent: '#7c3aed', racks: 2, rate: 1 };
var NS = 'http://www.w3.org/2000/svg';
var css =
'[data-server-rack-blink]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
'.srb-svg{position:absolute;inset:0;width:100%;height:100%;display:block;overflow:visible}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function rnd(a, b) { return a + Math.random() * (b - a); }
// How each kind of light behaves: [min on, max on, min off, max off] in ms.
var KINDS = {
disk: [40, 160, 80, 900],
port: [30, 110, 40, 1400],
warn: [180, 260, 2500, 9000],
beat: [600, 600, 900, 900]
};
// Rack contents, top to bottom.
var LAYOUTS = [
['switch', 'server', 'server', 'blank', 'storage', 'server', 'server'],
['server', 'switch', 'storage', 'server', 'server', 'blank', 'server']
];
function build(el) {
var d = el.dataset;
var act = d.color || DEFAULTS.color;
var acc = d.accent || DEFAULTS.accent;
var racks = d.racks === '1' ? 1 : DEFAULTS.racks;
var rate = d.rate !== undefined ? +d.rate : DEFAULTS.rate;
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'Status lights blinking on a rack of servers');
if (!el.hasAttribute('role')) el.setAttribute('role', 'img');
var W = 86, GAP = 12, UH = 24;
var total = racks * W + (racks - 1) * GAP;
var x0 = 200 - total / 2;
var lights = []; // [x, y, r, kind, colour]
var html = '<svg class="srb-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
'<ellipse cx="200" cy="232" rx="' + (total / 2 + 30) + '" ry="7" fill="#1e1b2e" opacity=".12"/>';
for (var r = 0; r < racks; r++) {
var rx = x0 + r * (W + GAP), top = 20;
html += '<rect x="' + rx + '" y="' + top + '" width="' + W + '" height="206" rx="6" fill="#1e1b2e"/>' +
'<rect x="' + (rx + 3) + '" y="' + (top + 3) + '" width="' + (W - 6) + '" height="8" rx="3" fill="#2d2a40"/>' +
'<rect x="' + (rx + 4) + '" y="' + (top + 5) + '" width="10" height="4" rx="2" fill="' + acc + '"/>' +
'<rect x="' + (rx + 6) + '" y="226" width="12" height="5" rx="1.5" fill="#1e1b2e"/><rect x="' + (rx + W - 18) + '" y="226" width="12" height="5" rx="1.5" fill="#1e1b2e"/>' +
'<rect x="' + (rx + 3) + '" y="' + (top + 13) + '" width="3" height="186" fill="#3b3754"/><rect x="' + (rx + W - 6) + '" y="' + (top + 13) + '" width="3" height="186" fill="#3b3754"/>';
var ux = rx + 8, uw = W - 16;
LAYOUTS[r % 2].forEach(function (kind, u) {
var uy = top + 16 + u * (UH + 3);
if (kind === 'blank') {
html += '<rect x="' + ux + '" y="' + uy + '" width="' + uw + '" height="' + UH + '" rx="2" fill="#2a2740"/>';
for (var v = 0; v < 4; v++) html += '<rect x="' + (ux + 6) + '" y="' + (uy + 5 + v * 4) + '" width="' + (uw - 12) + '" height="1.6" rx=".8" fill="#1e1b2e"/>';
return;
}
html += '<rect x="' + ux + '" y="' + uy + '" width="' + uw + '" height="' + UH + '" rx="2.5" fill="#475569"/>' +
'<rect x="' + ux + '" y="' + uy + '" width="' + uw + '" height="2" rx="1" fill="#fff" opacity=".12"/>';
if (kind === 'server') {
// Vents, two drive bays, then power and activity lights.
for (var g = 0; g < 5; g++) html += '<rect x="' + (ux + 4 + g * 3.4) + '" y="' + (uy + 5) + '" width="1.6" height="14" rx=".8" fill="#334155"/>';
for (var b = 0; b < 2; b++) {
var bx = ux + 24 + b * 15;
html += '<rect x="' + bx + '" y="' + (uy + 6) + '" width="13" height="12" rx="1.5" fill="#334155"/>';
lights.push([bx + 10, uy + 15, 1.3, 'disk', act]);
}
lights.push([ux + uw - 12, uy + UH / 2, 1.9, 'on', act]);
// Some servers have an amber warning light that flashes now and then.
var warn = Math.random() < 0.35;
lights.push([ux + uw - 6, uy + UH / 2, 1.9, warn ? 'warn' : 'disk', warn ? '#f59e0b' : act]);
} else if (kind === 'switch') {
// Two rows of ports, each with a little link light.
for (var row = 0; row < 2; row++) {
for (var p = 0; p < 8; p++) {
var px = ux + 4 + p * 7.4, py = uy + 3.5 + row * 9.5;
html += '<rect x="' + px.toFixed(1) + '" y="' + py + '" width="5.6" height="5.4" rx=".8" fill="#1e293b"/>';
lights.push([px + 2.8, py + 7.4, 0.9, 'port', p % 5 === 4 ? '#f59e0b' : act]);
}
}
lights.push([ux + uw - 5, uy + 7, 1.6, 'beat', '#38bdf8']);
lights.push([ux + uw - 5, uy + 15, 1.6, 'on', act]);
} else {
// A disk shelf: four bays, each with its own light.
for (var s = 0; s < 4; s++) {
var sx = ux + 3 + s * 13;
html += '<rect x="' + sx + '" y="' + (uy + 3) + '" width="11" height="18" rx="1.5" fill="#334155"/>' +
'<rect x="' + (sx + 2) + '" y="' + (uy + 6) + '" width="7" height="1.4" fill="#475569"/><rect x="' + (sx + 2) + '" y="' + (uy + 9) + '" width="7" height="1.4" fill="#475569"/>';
lights.push([sx + 5.5, uy + 17, 1.3, 'disk', act]);
}
lights.push([ux + uw - 6, uy + 8, 1.6, 'on', '#38bdf8']);
lights.push([ux + uw - 6, uy + 16, 1.6, 'disk', act]);
}
});
}
html += '<g class="srb-lights"></g></svg>';
el.innerHTML = html;
var lg = el.querySelector('.srb-lights');
var leds = lights.map(function (l) {
var halo = document.createElementNS(NS, 'circle');
halo.setAttribute('cx', l[0]); halo.setAttribute('cy', l[1]); halo.setAttribute('r', l[2] * 2.6);
halo.setAttribute('fill', l[4]); halo.setAttribute('opacity', 0);
var dot = document.createElementNS(NS, 'circle');
dot.setAttribute('cx', l[0]); dot.setAttribute('cy', l[1]); dot.setAttribute('r', l[2]);
dot.setAttribute('fill', l[4]);
lg.appendChild(halo);
lg.appendChild(dot);
var led = { dot: dot, halo: halo, kind: l[3], on: false, wait: rnd(0, 800) };
show(led, l[3] === 'on' || Math.random() < 0.4);
return led;
});
function show(led, on) {
led.on = on;
led.dot.setAttribute('opacity', on ? 1 : 0.16);
led.halo.setAttribute('opacity', on ? 0.3 : 0);
}
function step(dt) {
leds.forEach(function (led) {
var k = KINDS[led.kind];
if (!k) return; // steady lights
led.wait -= dt;
if (led.wait > 0) return;
show(led, !led.on);
led.wait = led.on ? rnd(k[0], k[1]) : rnd(k[2], k[3]);
// Disk and port lights flicker in bursts: short gaps, then a rest.
if (!led.on && led.kind !== 'warn' && led.kind !== 'beat' && Math.random() < 0.7) led.wait = rnd(30, 120);
});
}
if (reduced()) return;
var last = 0;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
step((last ? Math.min(50, now - last) : 16) * rate);
last = now;
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-server-rack-blink]').forEach(function (el) {
if (el._srb) return; // idempotent
el._srb = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-server-rack-blink></div>
<h2>All systems running</h2>
<!-- one busy rack: -->
<div data-server-rack-blink data-racks="1" data-rate="2"></div>