Animated Pattern Tiles
A repeating pattern of stripes, dots, zigzags, checks, waves or little crosses slides endlessly behind a section, like a moving wallpaper. Your own background colour shows through the gaps, so it works with any brand colour.
Stripes
Dots
Zigzags
How to use it
- Paste the snippet just before
</head>. - Add
data-animated-pattern-tilesto a section with a background colour. - Pick a pattern with
data-pattern.
Options
Add any of these attributes to the same element.
data-pattern | stripes, dots, zigzag, checks, waves or plus. Default stripes. |
data-color | Pattern colour. Default translucent white rgba(255,255,255,.16). |
data-size | Tile size in px. Default 32. |
data-speed | Speed in px per second. Default 20. |
data-angle | Direction of travel in degrees: 0 is right, 90 is down. Default 30. |
data-fade | Fade the pattern out towards the edges. |
The pattern is a tiny SVG tile repeated by the browser, so it stays sharp at any size and costs almost nothing. It pauses when off screen. With reduced motion it holds still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Animated Pattern Tiles: a repeating pattern of stripes, dots, zigzags,
// checks, waves or crosses slides endlessly behind a section, like a
// moving wallpaper. Your own background colour shows through the gaps.
// Usage: <section data-animated-pattern-tiles style="background:#6a0dad">…</section>
// Optional: data-pattern (stripes, dots, zigzag, checks, waves or plus,
// default stripes), data-color (pattern colour, default translucent white
// rgba(255,255,255,.16)), data-size (tile size in px, default 32),
// data-speed (px per second, default 20), data-angle (direction of travel
// in degrees, 0 is to the right, 90 is down, default 30), data-fade (fade
// the pattern out towards the edges).
var DEFAULTS = { pattern: 'stripes', color: 'rgba(255,255,255,.16)', size: 32, speed: 20, angle: 30 };
var css =
'[data-animated-pattern-tiles]{position:relative;isolation:isolate;overflow:hidden}' +
'.apt-layer{position:absolute;inset:0;z-index:-1;pointer-events:none;background-repeat:repeat}' +
'.apt-fade{-webkit-mask-image:radial-gradient(ellipse 70% 70% at 50% 50%,#000 30%,transparent 100%);mask-image:radial-gradient(ellipse 70% 70% at 50% 50%,#000 30%,transparent 100%)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// Each pattern is one square SVG tile of side s that repeats seamlessly.
var PATTERNS = {
stripes: function (s, c) {
var h = s / 2;
return '<path fill="' + c + '" d="M0 0H' + h + 'L0 ' + h + 'Z M' + s + ' 0V' + h + 'L' + h + ' ' + s + 'H0Z"/>';
},
dots: function (s, c) {
var r = s * 0.13;
return '<circle fill="' + c + '" cx="' + s / 4 + '" cy="' + s / 4 + '" r="' + r + '"/>' +
'<circle fill="' + c + '" cx="' + (s * 3) / 4 + '" cy="' + (s * 3) / 4 + '" r="' + r + '"/>';
},
zigzag: function (s, c) {
return '<path fill="none" stroke="' + c + '" stroke-width="' + s * 0.13 + '" stroke-linejoin="miter" d="M' + -s / 2 + ' ' + s * 0.35 +
'L0 ' + s * 0.65 + 'L' + s / 2 + ' ' + s * 0.35 + 'L' + s + ' ' + s * 0.65 + 'L' + s * 1.5 + ' ' + s * 0.35 + '"/>';
},
checks: function (s, c) {
var h = s / 2;
return '<path fill="' + c + '" d="M0 0H' + h + 'V' + h + 'H0Z M' + h + ' ' + h + 'H' + s + 'V' + s + 'H' + h + 'Z"/>';
},
waves: function (s, c) {
return '<path fill="none" stroke="' + c + '" stroke-width="' + s * 0.1 + '" d="M0 ' + s / 2 + 'C' + s / 4 + ' ' + s * 0.15 + ' ' + s / 4 + ' ' + s * 0.15 + ' ' + s / 2 + ' ' + s / 2 +
'S' + (s * 3) / 4 + ' ' + s * 0.85 + ' ' + s + ' ' + s / 2 + '"/>';
},
plus: function (s, c) {
var a = s * 0.07, b = s * 0.2, m = s / 2;
return '<path fill="' + c + '" d="M' + (m - a) + ' ' + (m - b) + 'H' + (m + a) + 'V' + (m - a) + 'H' + (m + b) + 'V' + (m + a) + 'H' + (m + a) +
'V' + (m + b) + 'H' + (m - a) + 'V' + (m + a) + 'H' + (m - b) + 'V' + (m - a) + 'H' + (m - a) + 'Z"/>';
}
};
function tile(name, s, color) {
var draw = PATTERNS[name] || PATTERNS[DEFAULTS.pattern];
var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + s + '" height="' + s + '" viewBox="0 0 ' + s + ' ' + s + '">' + draw(s, color) + '</svg>';
return 'url("data:image/svg+xml,' + encodeURIComponent(svg) + '")';
}
function build(el) {
var d = el.dataset;
var size = +d.size || DEFAULTS.size;
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
var angle = ((d.angle !== undefined ? +d.angle : DEFAULTS.angle) * Math.PI) / 180;
var layer = document.createElement('div');
layer.className = 'apt-layer' + (d.fade !== undefined && d.fade !== 'false' ? ' apt-fade' : '');
layer.setAttribute('aria-hidden', 'true');
layer.style.backgroundImage = tile(d.pattern || DEFAULTS.pattern, size, d.color || DEFAULTS.color);
layer.style.backgroundSize = size + 'px ' + size + 'px';
el.insertBefore(layer, el.firstChild);
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches || !speed) return;
var vx = Math.cos(angle) * speed, vy = Math.sin(angle) * speed, t = 0, last = 0;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
t += (last ? Math.min(50, now - last) : 16) / 1000;
last = now;
// The tile repeats every `size` px, so wrap to keep numbers small.
var x = ((vx * t) % size + size) % size, y = ((vy * t) % size + size) % size;
layer.style.backgroundPosition = x.toFixed(2) + 'px ' + y.toFixed(2) + 'px';
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-animated-pattern-tiles]').forEach(function (el) {
if (el._apt) return;
el._apt = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-animated-pattern-tiles data-pattern="dots" style="background: #6a0dad; color: #fff">
<h1>On repeat</h1>
</section>