← Backgrounds

Silk Waves

Glossy folds of satin roll slowly across a section, with soft shadows in the troughs and a bright sheen sliding along each crest. Pick the fabric colour to match your brand.

Smooth as silk

How to use it

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

Options

Add any of these attributes to the same element.

data-colorThe fabric colour. Default violet #7c3aed.
data-speedSpeed multiplier. Default 1.
data-scaleSize of the folds. Default 1.
data-angleDirection of the folds, in degrees. Default -20.

It’s drawn small on a canvas and scaled up (silk is smooth, so it stays clean), and it pauses when off screen. With reduced motion it shows one still frame.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Silk Waves: glossy folds of satin roll slowly across a section, with
    // soft shadows in the troughs and bright sheen along the crests. Drawn
    // small on a canvas behind your content (silk is smooth, so it scales up
    // cleanly), and paused when off screen.
    // Usage: <section data-silk-waves>…</section>
    // Optional: data-color (the fabric colour, default violet #7c3aed),
    // data-speed (default 1), data-scale (size of the folds, default 1),
    // data-angle (direction of the folds in degrees, default -20).
    var DEFAULTS = { color: '#7c3aed', speed: 1, scale: 1, angle: -20 };
    var MAX_PIXELS = 36000;   // the canvas never has more pixels than this

    var css =
      '[data-silk-waves]{position:relative;isolation:isolate;overflow:hidden}' +
      '.slk-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // Any CSS colour to [r, g, b], via a canvas.
    var probe = document.createElement('canvas').getContext('2d');
    function rgb(c) {
      probe.fillStyle = '#000';
      probe.fillStyle = c;
      var v = probe.fillStyle;
      if (v[0] === '#') return [parseInt(v.slice(1, 3), 16), parseInt(v.slice(3, 5), 16), parseInt(v.slice(5, 7), 16)];
      return v.match(/[\d.]+/g).slice(0, 3).map(Number);
    }

    function build(el) {
      var d = el.dataset;
      var base = rgb(d.color || DEFAULTS.color);
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var scale = +d.scale || DEFAULTS.scale;
      var angle = (d.angle !== undefined ? +d.angle : DEFAULTS.angle) * Math.PI / 180;
      var ca = Math.cos(angle), sa = Math.sin(angle);
      var canvas = document.createElement('canvas');
      canvas.className = 'slk-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var w = 0, h = 0, step = 4, img = null, t = 12;
      // Shadow and highlight versions of the fabric colour.
      var dark = base.map(function (c) { return c * 0.18; });
      var lite = base.map(function (c) { return c + (255 - c) * 0.35; });

      function resize() {
        var cw = Math.max(1, el.clientWidth), ch = Math.max(1, el.clientHeight);
        step = Math.max(3, Math.sqrt((cw * ch) / MAX_PIXELS));
        w = Math.ceil(cw / step);
        h = Math.ceil(ch / step);
        canvas.width = w;
        canvas.height = h;
        img = ctx.createImageData(w, h);
        draw();
      }

      function draw() {
        var data = img.data;
        // Fold size in canvas pixels: about 300 css px across a fold pair.
        var S = (300 * scale) / step / (Math.PI * 2);
        for (var y = 0; y < h; y++) {
          for (var x = 0; x < w; x++) {
            // u runs across the folds, v along them.
            var u = (x * ca + y * sa) / S, v = (-x * sa + y * ca) / S;
            // Two layers of folds, each bent by slow waves along its length.
            var p1 = u * (1 + 0.18 * Math.sin(v * 0.3 + t * 0.11)) + 1.5 * Math.sin(v * 0.32 + t * 0.29) + t * 0.4;
            var p2 = u * 1.7 - 1.1 * Math.sin(v * 0.5 - t * 0.21 + u * 0.25) - t * 0.23;
            // The slope of the cloth facing the light.
            var s = 0.75 * Math.cos(p1) + 0.25 * Math.cos(p2);
            var diff = 0.5 + 0.5 * s;
            diff = diff * diff * (3 - 2 * diff);
            // A narrow, bright sheen on the crests is what makes it read as satin.
            var sp = s > 0.55 ? (s - 0.55) / 0.45 : 0;
            sp = sp * sp * sp * 0.75;
            var o = (y * w + x) * 4;
            data[o] = dark[0] + (lite[0] - dark[0]) * diff + (255 - lite[0]) * sp;
            data[o + 1] = dark[1] + (lite[1] - dark[1]) * diff + (255 - lite[1]) * sp;
            data[o + 2] = dark[2] + (lite[2] - dark[2]) * diff + (255 - lite[2]) * sp;
            data[o + 3] = 255;
          }
        }
        ctx.putImageData(img, 0, 0);
      }

      resize();
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
        if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
        return;
      }
      if (window.ResizeObserver) new ResizeObserver(resize).observe(el);

      var last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = last ? Math.min(50, now - last) : 16;
          last = now;
          t += (dt / 1000) * speed;
          draw();
        }
      });
      // Pause while off screen to save battery.
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-silk-waves]').forEach(function (el) {
        if (el._slk) return;
        el._slk = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-silk-waves data-color="#be185d" style="color: #fff">
  <h1>Smooth as silk</h1>
</section>