← SVG & icons

Wave Section Divider

A wavy edge between two sections ripples gently, with fainter waves layered behind it for depth. Each wave is two swells drifting at different speeds, so the shape keeps changing without ever looking like a loop.

Section one
Section two

How to use it

  1. Paste the snippet just before </head>.
  2. Put an empty <div data-wave-section-divider> at the very bottom of the upper section.
  3. Give it the lower section’s background colour with data-color (or color), so the waves read as the next section rising up.

Options

Add any of these attributes to the same element.

data-colorColour of the waves. Default the text colour.
data-heightHeight of the divider. Default 80 px.
data-layersWaves stacked up, the front one solid and the rest fainter. Default 3.
data-amplitudeHow tall the swells are, from 0 to 1 of the height. Default 0.35.
data-speedHow fast they drift. Default 1.
data-flipFill upwards instead, for a divider at the top of the lower section.

The waves stretch to any width. They pause while off screen, and hold still with reduced motion.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Wave Section Divider: a wavy edge between two sections that ripples
    // gently, with softer waves layered behind it for depth. Put it at the
    // bottom of the upper section (or the top of the lower one) and give it the
    // lower section's colour.
    // Usage: <div data-wave-section-divider style="color: #f5f3ff"></div>
    // Optional: data-color (fill colour, default the text colour), data-height
    // (px, default 80), data-layers (waves, front one solid and the rest
    // fainter behind it, default 3), data-amplitude (how tall the waves are, 0
    // to 1 of the height, default 0.35), data-speed (default 1), data-flip
    // (fill upwards instead, for the top of a section).
    var DEFAULTS = { height: 80, layers: 3, amplitude: 0.35, speed: 1 };
    var NS = 'http://www.w3.org/2000/svg';
    var W = 1200, STEP = 20;

    var css =
      '.wsd{display:block;position:relative;width:100%;line-height:0;pointer-events:none}' +
      '.wsd-svg{display:block;width:100%;height:100%}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function setup(el) {
      var d = el.dataset;
      var H = +d.height || DEFAULTS.height;
      el.classList.add('wsd');
      el.setAttribute('aria-hidden', 'true');
      if (!el.style.height) el.style.height = H + 'px';
      var svg = document.createElementNS(NS, 'svg');
      svg.setAttribute('class', 'wsd-svg');
      svg.setAttribute('viewBox', '0 0 ' + W + ' 100');
      svg.setAttribute('preserveAspectRatio', 'none');
      svg.setAttribute('fill', d.color || 'currentColor');
      var n = Math.max(1, +d.layers || DEFAULTS.layers);
      var layers = [];
      for (var i = 0; i < n; i++) {
        var p = document.createElementNS(NS, 'path');
        var back = n - 1 - i; // 0 is the front wave, drawn last
        p.setAttribute('opacity', back ? Math.max(0.15, 0.5 - (back - 1) * 0.18) : 1);
        svg.appendChild(p);
        layers.push({
          el: p,
          // Back waves sit a little higher, with their own rhythm and drift.
          base: 60 - back * 11,
          k1: (Math.PI * 2 * (1.4 + back * 0.35)) / W,
          k2: (Math.PI * 2 * (2.7 - back * 0.4)) / W,
          s1: (0.55 + back * 0.12) * (back % 2 ? -1 : 1),
          s2: -0.9 + back * 0.2,
          ph: back * 1.7
        });
      }
      el.appendChild(svg);
      return { el: el, svg: svg, layers: layers, flip: d.flip !== undefined && d.flip !== 'false', speed: +d.speed || DEFAULTS.speed,
        amp: (d.amplitude !== undefined ? +d.amplitude : DEFAULTS.amplitude) * 50 };
    }

    function draw(o, t) {
      o.layers.forEach(function (l) {
        var pts = [];
        for (var x = 0; x <= W; x += STEP) {
          var y = l.base +
            o.amp * 0.62 * Math.sin(l.k1 * x + t * l.s1 + l.ph) +
            o.amp * 0.38 * Math.sin(l.k2 * x + t * l.s2 + l.ph * 2.3);
          y = Math.max(0, Math.min(100, y));
          pts.push(x + ',' + (o.flip ? 100 - y : y).toFixed(1));
        }
        var edge = o.flip ? '0' : '100';
        l.el.setAttribute('d', 'M0,' + edge + ' L' + pts.join(' L') + ' L' + W + ',' + edge + ' Z');
      });
    }

    function start(o) {
      var visible = false, raf = 0, t0 = performance.now(), offset = Math.random() * 20;
      var frame = function (now) {
        raf = 0;
        if (!visible) return;
        draw(o, offset + ((now - t0) / 1000) * o.speed);
        raf = requestAnimationFrame(frame);
      };
      new IntersectionObserver(function (entries) {
        visible = entries[0].isIntersecting;
        if (visible && !raf) raf = requestAnimationFrame(frame);
      }).observe(o.el);
    }

    function init() {
      document.querySelectorAll('[data-wave-section-divider]').forEach(function (el) {
        if (el._wsd) return;
        var o = (el._wsd = setup(el));
        draw(o, 2);
        if (!reduced()) start(o);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section class="hero" style="background: #6d28d9">
  …
  <div data-wave-section-divider data-color="#faf5ff"></div>
</section>
<section style="background: #faf5ff">…</section>