Pixel Snake
A little snake of lit pixels slithers round a small grid, its head brightest and its tail fading out behind it, like a retro handheld game waiting to start. Send it round every pixel in a loop, spiralling in and out, or zigzagging along the rows. 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-pixel-snake>where the loader should appear.
Options
Add any of these attributes to the same element.
data-size | Pixels along each side, 3 to 8. Default 4. The loop needs an even number, so odd sizes go up by one. |
data-length | Pixels in the snake. Default 4. |
data-speed | Time for each step. Default 110 ms. |
data-path | loop (round every pixel and back to the start), spiral (in to the middle and out again) or zigzag (along the rows and back). Default loop. |
data-head | A different colour for the snake’s head. |
Each pixel is 0.42em with a 0.12em gap, so the default grid is about 2em square. It only moves while it’s on screen. With reduced motion the snake sits still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Pixel Snake: a little snake of lit pixels slithers round a small grid,
// its head brightest and its tail fading out behind it, like a retro
// handheld game waiting to start.
// Sized with font-size (each pixel is 0.42em), coloured with color.
// It only moves while it's on screen.
// Usage: <div data-pixel-snake aria-label="Loading"></div>
// Optional: data-size (pixels along each side, 3 to 8, default 4),
// data-length (pixels in the snake, default 4), data-speed (ms per step,
// default 110), data-path ("loop" round every pixel and back to the start,
// "spiral" in to the middle and out again, or "zigzag" along the rows and
// back; default "loop"), data-head (a different colour for the head).
var DEFAULTS = { size: 4, length: 4, speed: 110, path: 'loop' };
var css =
'.pxn{display:inline-grid;gap:.12em;vertical-align:middle}' +
'.pxn-px{display:block;width:.42em;height:.42em;border-radius:.09em;background:currentColor;opacity:.12;transform:scale(.72);transition-property:opacity,transform,background-color;transition-timing-function:ease-out}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// A loop through every cell that ends next to where it began. Needs an even size.
function loopPath(n) {
var p = [], r, c;
for (c = 0; c < n; c++) p.push([0, c]);
for (r = 1; r < n; r++) {
if (r % 2) for (c = n - 1; c >= 1; c--) p.push([r, c]);
else for (c = 1; c < n; c++) p.push([r, c]);
}
for (r = n - 1; r >= 1; r--) p.push([r, 0]);
return p;
}
function spiralPath(n) {
var p = [], top = 0, left = 0, bottom = n - 1, right = n - 1, i;
while (top <= bottom && left <= right) {
for (i = left; i <= right; i++) p.push([top, i]);
for (i = top + 1; i <= bottom; i++) p.push([i, right]);
if (top < bottom) for (i = right - 1; i >= left; i--) p.push([bottom, i]);
if (left < right) for (i = bottom - 1; i > top; i--) p.push([i, left]);
top++; left++; bottom--; right--;
}
return p;
}
function zigzagPath(n) {
var p = [];
for (var r = 0; r < n; r++) for (var c = 0; c < n; c++) p.push([r, r % 2 ? n - 1 - c : c]);
return p;
}
// Ping-pong a one-way path so the snake turns round at each end.
function bounce(p) { return p.concat(p.slice(1, -1).reverse()); }
function build(el) {
var d = el.dataset;
var kind = d.path || DEFAULTS.path;
var n = Math.max(3, Math.min(8, Math.round(+d.size) || DEFAULTS.size));
if (kind === 'loop' && n % 2) n++;
var len = Math.max(2, Math.min(n * n - 1, Math.round(+d.length) || DEFAULTS.length));
var speed = Math.max(40, +d.speed || DEFAULTS.speed);
var path = kind === 'spiral' ? bounce(spiralPath(n)) : kind === 'zigzag' ? bounce(zigzagPath(n)) : loopPath(n);
el.classList.add('pxn');
if (!el.getAttribute('role')) el.setAttribute('role', 'progressbar');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Loading');
el.textContent = '';
el.style.gridTemplateColumns = 'repeat(' + n + ', auto)';
var cells = [];
for (var i = 0; i < n * n; i++) {
var s = document.createElement('span');
s.className = 'pxn-px';
s.setAttribute('aria-hidden', 'true');
s.style.transitionDuration = Math.round(speed * 1.6) + 'ms';
el.appendChild(s);
cells.push(s);
}
var lit = [];
function draw(step) {
var level = {};
for (var k = 0; k < len; k++) {
var at = path[((step - k) % path.length + path.length) % path.length];
var idx = at[0] * n + at[1];
var v = 1 - k / len;
if (!(idx in level) || level[idx] < v) level[idx] = v;
}
lit.forEach(function (idx) { if (!(idx in level)) paint(idx, 0, false); });
lit = Object.keys(level).map(Number);
var head = path[((step % path.length) + path.length) % path.length];
lit.forEach(function (idx) { paint(idx, level[idx], idx === head[0] * n + head[1]); });
}
function paint(idx, v, isHead) {
var c = cells[idx];
c.style.opacity = v ? (0.18 + 0.82 * v).toFixed(2) : '';
c.style.transform = v ? 'scale(' + (0.78 + 0.22 * v).toFixed(2) + ')' : '';
c.style.backgroundColor = isHead && d.head ? d.head : '';
}
var step = len - 1;
draw(step);
if (reduced()) return null;
var acc = 0, last = 0;
return anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
acc += last ? Math.min(200, now - last) : 0;
last = now;
while (acc >= speed) { acc -= speed; step++; draw(step); }
},
onPause: function () { last = 0; },
});
}
function init() {
document.querySelectorAll('[data-pixel-snake]').forEach(function (el) {
if (el._pxn) return;
el._pxn = 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-pixel-snake style="font-size: 24px; color: #6a0dad" aria-label="Loading"></div>
<!-- a longer snake spiralling round a bigger grid: -->
<div data-pixel-snake data-path="spiral" data-size="5" data-length="7"></div>