Floating Shapes
Soft, playful shapes (circles, rings, squares, triangles, crosses and squiggles) drift and turn slowly behind a section. Near ones are bigger and brighter and move a little more; far ones are small and slightly blurred, for a gentle sense of depth.
Make something fun
How to use it
- Paste the snippet just before
</head>. - Add
data-floating-shapesto a section, hero or card.
Options
Add any of these attributes to the same element.
data-count | How many shapes. Default 14. |
data-colors | Comma-separated colours. Default soft violet, pink, amber, sky and mint. |
data-shapes | Which shapes to use, from circle, ring, square, triangle, plus and squiggle. Default all. |
data-speed | Seconds per drift. Default 7. |
data-opacity | Opacity of the nearest shapes. Default 0.75. |
Sizes follow the section’s shorter side, so it suits a card as well as a full-screen hero. It pauses when off screen; with reduced motion the shapes stay still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Floating Shapes: soft, playful shapes (circles, rings, squares,
// triangles, crosses and squiggles) drift and turn slowly behind a
// section. Nearer ones are bigger, brighter and quicker; far ones are
// small and slightly blurred, for a gentle sense of depth. Paused when off
// screen.
// Usage: <section data-floating-shapes>…</section>
// Optional: data-count (default 14), data-colors (comma-separated,
// default soft violet, pink, amber, sky and mint), data-shapes (which to
// use, e.g. "circle,ring,plus"), data-speed (seconds per drift, default 7),
// data-opacity (default 0.75).
var DEFAULTS = {
count: 14, speed: 7, opacity: 0.75,
colors: ['#c084fc', '#f472b6', '#fbbf24', '#38bdf8', '#34d399']
};
var SHAPES = {
circle: '<circle cx="50" cy="50" r="44" fill="{c}"/>',
ring: '<circle cx="50" cy="50" r="36" fill="none" stroke="{c}" stroke-width="15"/>',
square: '<rect x="10" y="10" width="80" height="80" rx="22" fill="{c}"/>',
triangle: '<path d="M50 14 L88 80 H12 Z" fill="{c}" stroke="{c}" stroke-width="14" stroke-linejoin="round"/>',
plus: '<path d="M50 14 V86 M14 50 H86" stroke="{c}" stroke-width="20" stroke-linecap="round"/>',
squiggle: '<path d="M8 58 Q22 28 36 50 T64 50 T92 42" fill="none" stroke="{c}" stroke-width="13" stroke-linecap="round"/>'
};
var css =
'[data-floating-shapes]{position:relative;isolation:isolate;overflow:hidden}' +
'.fs-layer{position:absolute;inset:0;z-index:-1;pointer-events:none;overflow:hidden}' +
'.fs-shape{position:absolute;display:block;width:calc(var(--fs-unit,300px) * var(--fs-size));aspect-ratio:1;margin:calc(var(--fs-unit,300px) * var(--fs-size) / -2)}' +
'.fs-shape svg{display:block;width:100%;height:100%;overflow:visible}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function build(el) {
var d = el.dataset;
var list = function (v, fallback) { return v ? v.split(',').map(function (s) { return s.trim(); }) : fallback; };
var colors = list(d.colors, DEFAULTS.colors);
var kinds = list(d.shapes, Object.keys(SHAPES)).filter(function (k) { return SHAPES[k]; });
if (!kinds.length) kinds = Object.keys(SHAPES);
var count = +d.count || DEFAULTS.count;
var speed = (+d.speed || DEFAULTS.speed) * 1000;
var opacity = d.opacity !== undefined ? +d.opacity : DEFAULTS.opacity;
var R = anime.utils.random;
var layer = document.createElement('div');
layer.className = 'fs-layer';
layer.setAttribute('aria-hidden', 'true');
el.insertBefore(layer, el.firstChild);
// Sizes follow the section's shorter side, so it works small or large.
var fit = function () { layer.style.setProperty('--fs-unit', Math.min(el.clientWidth, el.clientHeight) + 'px'); };
fit();
if (window.ResizeObserver) new ResizeObserver(fit).observe(el);
// Spread them out on a loose grid, so they never start in a clump.
var cols = Math.ceil(Math.sqrt(count * 1.6)), rows = Math.ceil(count / cols);
var shapes = [];
for (var i = 0; i < count; i++) {
var depth = R(0, 1, 2);
var s = document.createElement('span');
s.className = 'fs-shape';
s.innerHTML = '<svg viewBox="0 0 100 100">' + SHAPES[kinds[i % kinds.length]].replace(/\{c\}/g, colors[(i * 3) % colors.length]) + '</svg>';
s.style.setProperty('--fs-size', (0.08 + depth * 0.13).toFixed(3));
s.style.left = (((i % cols) + R(0.15, 0.85, 2)) / cols) * 100 + '%';
s.style.top = ((Math.floor(i / cols) + R(0.15, 0.85, 2)) / rows) * 100 + '%';
s.style.opacity = (opacity * (0.45 + depth * 0.55)).toFixed(2);
if (depth < 0.4) s.style.filter = 'blur(' + ((0.4 - depth) * 5).toFixed(1) + 'px)';
layer.appendChild(s);
shapes.push({ el: s, depth: depth, rotate: R(0, 360), anim: null });
anime.utils.set(s, { rotate: shapes[i].rotate });
}
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
shapes.forEach(function (sh) {
(function drift() {
// Near shapes wander further: up to a seventh of the shorter side.
var reach = Math.min(el.clientWidth, el.clientHeight) * (0.06 + sh.depth * 0.09);
sh.rotate += R(-90, 90);
sh.anim = anime.animate(sh.el, {
translateX: R(-reach, reach),
translateY: R(-reach, reach),
rotate: sh.rotate,
duration: speed * R(0.8, 1.2, 2) * (1.4 - sh.depth * 0.6),
ease: 'inOut(2)',
onComplete: drift
});
})();
});
new IntersectionObserver(function (entries) {
var on = entries[0].isIntersecting;
shapes.forEach(function (sh) { if (on) sh.anim.play(); else sh.anim.pause(); });
}).observe(el);
}
function init() {
document.querySelectorAll('[data-floating-shapes]').forEach(function (el) {
if (el._fs) return;
el._fs = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-floating-shapes data-shapes="circle,ring,plus">
<h1>Hello</h1>
</section>