← Backgrounds

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

  1. Paste the snippet just before </head>.
  2. Add data-floating-shapes to a section, hero or card.

Options

Add any of these attributes to the same element.

data-countHow many shapes. Default 14.
data-colorsComma-separated colours. Default soft violet, pink, amber, sky and mint.
data-shapesWhich shapes to use, from circle, ring, square, triangle, plus and squiggle. Default all.
data-speedSeconds per drift. Default 7.
data-opacityOpacity 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.

1 · Snippet
<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>
2 · Your HTML
<section data-floating-shapes data-shapes="circle,ring,plus">
  <h1>Hello</h1>
</section>