Copied!
Background

Silk flow draggable folds

Smooth silky folds of green and lime light that drift slowly and can be pulled by dragging, with floating Enrichr puzzle pieces on top.

← Library

Element · Vanilla JS + CanvasUpdated November 6, 2026

BackgroundElementVanilla JS + Canvas

Live preview needs JavaScript. The code below works without any dependencies.

Prompt for your AI assistant

Paste this into Claude, Cursor or ChatGPT. The settings update as you move the controls above.

Build "Silk flow draggable folds" for my site using the Enrichr Library element at https://enrichr.dev/elements/silk-flow.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/silk-flow.json (markup, CSS, JavaScript, parameters).
Requirements: vanilla JavaScript and CSS, no new dependencies; keep the behaviour of the source unchanged; respect prefers-reduced-motion; work on touch screens; keep it accessible.

Settings:
- accent: #e6ff55
- folds: 9
- softness: 0.6
- speed: 1

Verify: reduced motion shows no movement, it works at 375px wide, there are no console errors, and scrolling stays smooth.

The same spec as JSON, for tools that read it directly: /elements/silk-flow.json.

Code

Mount it on an element that contains the markup. It returns an object with update(options) and destroy().

HTML

<div class="ef-sf">
  <canvas class="ef-sf__c" role="img" aria-label="Smooth silky folds of green and lime light drifting slowly; drag to pull them"></canvas>
  <svg class="ef-sf__defs" width="0" height="0" aria-hidden="true" focusable="false"><defs><symbol id="ef-sf-p1" viewBox="1513.7 1453.9 322.3 322.3"><path fill="currentColor" d="M1821 1761.2h-292.3v-111.6c0-8 6.5-14.5 14.5-14.5 4.2 0 8.2 1.9 11 5.1 7.8 8.6 19.7 13.6 32.7 11.9 16.8-2.3 30.1-16 32-32.7 2.5-22.6-15.1-41.6-37.1-41.6-11 0-20.9 4.7-27.7 12.3-2.8 3.1-6.7 4.9-10.9 4.9-8 0-14.5-6.4-14.5-14.4v-111.7h292.3v111.7c0 8-6.4 14.4-14.5 14.4-4.2 0-8.1-1.8-10.9-5-7.9-8.6-19.7-13.6-32.7-11.9-16.8 2.2-30.1 15.9-32 32.7-2.5 22.6 15.1 41.6 37.1 41.6 11 0 20.8-4.8 27.7-12.3 2.8-3.1 6.7-5 10.8-5 8.1 0 14.5 6.5 14.5 14.5z"/></symbol><symbol id="ef-sf-p4" viewBox="1423.36 860.8 503.07 322.3"><path fill="currentColor" d="M1821 875.8v111.6c0 8 6.5 14.5 14.5 14.5 4.2 0 8.1-1.9 10.9-5 6.8-7.5 16.7-12.3 27.7-12.3 22 0 39.6 19 37.1 41.6-1.9 16.7-15.2 30.5-32 32.7-13 1.7-24.8-3.3-32.7-11.9-2.8-3.2-6.8-5-11-5-8 0-14.5 6.4-14.5 14.4v111.7h-111.6c-8 0-14.5-6.5-14.5-14.5 0-4.2 1.9-8.2 5-11 8.7-7.9 13.7-19.7 12-32.7-2.3-16.8-16-30.1-32.8-32-22.5-2.5-41.5 15.1-41.5 37.1 0 11 4.7 20.9 12.3 27.7 3.1 2.8 4.9 6.7 4.9 10.8v0.1c0 8-6.4 14.5-14.5 14.5h-111.6v-111.7c0-8-6.4-14.4-14.5-14.4-4.1 0-8 1.8-10.8 4.9-6.9 7.6-16.7 12.3-27.7 12.3-22 0-39.6-19-37.1-41.6 1.9-16.7 15.2-30.4 32-32.7 13-1.7 24.8 3.3 32.7 11.9 2.8 3.2 6.7 5.1 10.9 5.1 8.1 0 14.5-6.5 14.5-14.5v-111.6z"/></symbol><symbol id="ef-sf-p8" viewBox="1513.7 166.86 322.3 503.06"><path fill="currentColor" d="M1528.7 564.5v-111.6c0-8 6.5-14.5 14.5-14.5 4.2 0 8.2 1.9 11 5 7.8 8.7 19.7 13.7 32.7 11.9 16.8-2.2 30.1-15.9 32-32.7 2.5-22.5-15.1-41.5-37.1-41.5-11 0-20.9 4.7-27.7 12.3-2.8 3.1-6.7 4.9-10.9 4.9-8 0-14.5-6.5-14.5-14.5v-111.6h111.6c8.1 0 14.5-6.5 14.5-14.5 0-4.1-1.8-8.1-4.9-10.9-7.6-6.8-12.3-16.6-12.3-27.6 0-22 19-39.6 41.5-37.1 16.8 1.8 30.5 15.2 32.8 32 1.7 12.9-3.3 24.8-12 32.7-3.1 2.8-5 6.7-5 10.9 0 8 6.5 14.5 14.5 14.5h111.6v111.6c0 8-6.4 14.5-14.5 14.5-4.2 0-8.1-1.9-10.9-5-7.9-8.7-19.7-13.6-32.7-11.9-16.8 2.2-30.1 15.9-32 32.7-2.5 22.5 15.1 41.6 37.1 41.6 11 0 20.8-4.8 27.7-12.3 2.8-3.1 6.7-5 10.8-5 8.1 0 14.5 6.5 14.5 14.5v111.6h-111.6c-8 0-14.5 6.5-14.5 14.5 0 4.2 1.9 8.1 5 10.9 7.5 6.8 12.3 16.7 12.3 27.7 0 22-19 39.5-41.6 37.1-16.8-1.9-30.5-15.3-32.7-32-1.7-13 3.3-24.9 11.9-32.7 3.1-2.9 5-6.8 5-11 0-8-6.4-14.5-14.5-14.5z"/></symbol></defs></svg>
  <div class="ef-sf__pieces" aria-hidden="true"><svg class="ef-sf__pc ef-sf__pc--a" aria-hidden="true" focusable="false"><use href="#ef-sf-p1" width="100%" height="100%"/></svg><svg class="ef-sf__pc ef-sf__pc--b" aria-hidden="true" focusable="false"><use href="#ef-sf-p4" width="100%" height="100%"/></svg><svg class="ef-sf__pc ef-sf__pc--c" aria-hidden="true" focusable="false"><use href="#ef-sf-p8" width="100%" height="100%"/></svg></div>
</div>

CSS

/* Enrichr Library · https://enrichr.dev */
.ef-sf { position: relative; width: 100%; height: 100%; min-height: 18rem; overflow: hidden; background: #0c1e18; isolation: isolate; }
.ef-sf__c { position: absolute; inset: 0; width: 100%; height: 100%; display: block; touch-action: pan-y; cursor: grab; }
.ef-sf.is-drag .ef-sf__c { cursor: grabbing; }
.ef-sf__defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.ef-sf__pieces { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
.ef-sf__pc { position: absolute; display: block; filter: drop-shadow(0 10px 14px rgba(0, 0, 0, 0.35)); will-change: transform; }
.ef-sf__pc--a { width: min(22%, 6.5rem); aspect-ratio: 1; left: 9%; top: 14%; color: var(--ef-pa, #e6ff55); animation: ef-sf-f1 9s ease-in-out infinite; }
.ef-sf__pc--b { width: min(34%, 9rem); aspect-ratio: 503 / 322; right: 8%; bottom: 16%; color: var(--ef-pb, #fbf8f1); opacity: 0.92; animation: ef-sf-f2 11s ease-in-out infinite; }
.ef-sf__pc--c { width: min(15%, 4.4rem); aspect-ratio: 322 / 503; right: 22%; top: 9%; color: var(--ef-pc, #2f7d5b); animation: ef-sf-f3 13s ease-in-out infinite; }
.ef-sf.no-pieces .ef-sf__pieces { display: none; }
@keyframes ef-sf-f1 { 0%, 100% { transform: translate3d(0, 0, 0) rotate(-12deg); } 50% { transform: translate3d(8px, -14px, 0) rotate(-4deg); } }
@keyframes ef-sf-f2 { 0%, 100% { transform: translate3d(0, 0, 0) rotate(8deg); } 50% { transform: translate3d(-12px, 10px, 0) rotate(15deg); } }
@keyframes ef-sf-f3 { 0%, 100% { transform: translate3d(0, 0, 0) rotate(18deg); } 50% { transform: translate3d(6px, 12px, 0) rotate(8deg); } }
@media (prefers-reduced-motion: reduce) { .ef-sf__pc { animation: none; } .ef-sf__pc--a { transform: rotate(-10deg); } .ef-sf__pc--b { transform: rotate(8deg); } .ef-sf__pc--c { transform: rotate(16deg); } }

JavaScript

// Enrichr Library · https://enrichr.dev
// Silk flow: smooth folds of light drawn as layered translucent ribbons. Each ribbon is a band between two sine curves filled with a soft vertical gradient, with a thin highlight along its crest, so the stack reads like draped silk.
// The folds drift slowly; press and drag (mouse, pen or touch) to pull them, and they ease back to drifting when you let go. An original rebuild from the general idea.
// Reduced motion draws one still frame. The loop pauses off screen.
// mount(root, { background, accent, folds (4-16), softness (0-1), speed (0-3), drag (0-3), pieces ("on"|"off") })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var wrap = root.querySelector(".ef-sf");
  var canvas = root.querySelector(".ef-sf__c");
  var ctx = canvas.getContext("2d");
  var opts = {}, raf = 0, visible = true, io = null, ro = null, destroyed = false, last = 0, t = 0;
  var W = 1, H = 1, dpr = 1;
  var dragging = false, lx = 0, ly = 0, offX = 0, offY = 0, velX = 0, velY = 0;
  var acc = [230, 255, 85];
  var GREENS = [[0, 82, 54], [10, 107, 70], [47, 125, 91]];

  function rgb(h, d) { var m = /^#?([0-9a-f]{6})$/i.exec(h || ""); var v = parseInt(m ? m[1] : d, 16); return [v >> 16, (v >> 8) & 255, v & 255]; }
  function c(a, al) { return "rgba(" + (a[0] | 0) + "," + (a[1] | 0) + "," + (a[2] | 0) + "," + al.toFixed(3) + ")"; }
  function size() {
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    W = Math.max(1, wrap.clientWidth); H = Math.max(1, wrap.clientHeight);
    canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  function curve(i, n, x, ph, oy) {
    var u = x / W, k = i / n;
    var amp = H * (0.07 + 0.05 * Math.sin(i * 1.7));
    return H * (0.12 + 0.8 * k) + oy * 60
      + amp * Math.sin(u * 3.3 + ph + i * 0.55 + offX * 2.2)
      + amp * 0.45 * Math.sin(u * 7.1 - ph * 1.3 + i * 1.2 - offX * 1.4)
      + amp * 0.3 * Math.sin(u * 1.4 + ph * 0.6 - i * 0.4 + offY * 3);
  }
  function draw() {
    ctx.globalCompositeOperation = "source-over";
    ctx.fillStyle = opts.background; ctx.fillRect(0, 0, W, H);
    var n = opts.folds, soft = opts.softness, step = Math.max(6, Math.round(W / 90));
    var ph = t * 0.55;
    var xs = [];
    for (var x = 0; x <= W + step; x += step) xs.push(x);
    ctx.globalCompositeOperation = "lighter";
    for (var i = 0; i < n; i++) {
      var g1 = GREENS[i % 3], k = i / Math.max(1, n - 1);
      var top = [], bot = [], ymin = 1e9, ymax = -1e9, j, y;
      for (j = 0; j < xs.length; j++) {
        y = curve(i, n, xs[j], ph, offY);
        var thick = H * (0.1 + 0.16 * soft) * (0.8 + 0.4 * Math.sin(xs[j] / W * 4 + i + ph * 0.7));
        top.push(y - thick * 0.5); bot.push(y + thick * 0.5);
        if (top[j] < ymin) ymin = top[j]; if (bot[j] > ymax) ymax = bot[j];
      }
      var gr = ctx.createLinearGradient(0, ymin, 0, ymax);
      var mix = [g1[0] + (acc[0] - g1[0]) * k * 0.55, g1[1] + (acc[1] - g1[1]) * k * 0.55, g1[2] + (acc[2] - g1[2]) * k * 0.55];
      gr.addColorStop(0, c(mix, 0));
      gr.addColorStop(0.5, c(mix, 0.2 + 0.16 * soft));
      gr.addColorStop(1, c(mix, 0));
      ctx.fillStyle = gr;
      ctx.beginPath();
      for (j = 0; j < xs.length; j++) { if (j === 0) ctx.moveTo(xs[j], top[j]); else ctx.lineTo(xs[j], top[j]); }
      for (j = xs.length - 1; j >= 0; j--) ctx.lineTo(xs[j], bot[j]);
      ctx.closePath(); ctx.fill();
      // sheen along the crest
      ctx.strokeStyle = c(i % 2 ? [251, 248, 241] : acc, 0.1 + 0.16 * (1 - soft) + 0.04 * k);
      ctx.lineWidth = 1;
      ctx.beginPath();
      for (j = 0; j < xs.length; j++) { y = (top[j] + bot[j]) / 2 - (bot[j] - top[j]) * 0.08; if (j === 0) ctx.moveTo(xs[j], y); else ctx.lineTo(xs[j], y); }
      ctx.stroke();
    }
    ctx.globalCompositeOperation = "source-over";
  }
  function frame(now) {
    raf = 0;
    if (destroyed || !visible) return;
    var dt = last ? Math.min(0.05, (now - last) / 1000) : 0.016;
    last = now;
    t += dt * opts.speed;
    if (!dragging) {
      offX += velX * dt; offY += velY * dt;
      var d = Math.exp(-dt * 2.2); velX *= d; velY *= d;
      // ease the pulled offset back to rest
      offX *= Math.exp(-dt * 0.35); offY *= Math.exp(-dt * 0.35);
    }
    draw();
    raf = requestAnimationFrame(frame);
  }
  function start() { if (!reduce && !raf && visible && !destroyed) { last = 0; raf = requestAnimationFrame(frame); } }
  function stop() { if (raf) cancelAnimationFrame(raf); raf = 0; }
  function down(e) {
    dragging = true; lx = e.clientX; ly = e.clientY; velX = velY = 0;
    wrap.classList.add("is-drag");
    try { canvas.setPointerCapture(e.pointerId); } catch (x) { }
  }
  function move(e) {
    if (!dragging) return;
    var dx = (e.clientX - lx) / W * opts.drag, dy = (e.clientY - ly) / H * opts.drag;
    lx = e.clientX; ly = e.clientY;
    offX += dx * 1.6; offY += dy * 1.2;
    velX = dx * 20; velY = dy * 16;
    if (reduce) draw();
  }
  function up(e) {
    if (!dragging) return;
    dragging = false; wrap.classList.remove("is-drag");
    try { canvas.releasePointerCapture(e.pointerId); } catch (x) { }
  }
  function update(next) {
    opts = Object.assign({ background: "#0c1e18", accent: "#e6ff55", folds: 9, softness: 0.6, speed: 1, drag: 1.4, pieces: "on" }, opts, next || {});
    opts.folds = Math.max(4, Math.min(16, Math.round(opts.folds)));
    acc = rgb(opts.accent, "e6ff55");
    wrap.classList.toggle("no-pieces", String(opts.pieces) === "off");
    if (reduce) { t = 1.6; draw(); } else { draw(); start(); }
  }
  size();
  update(o);
  canvas.addEventListener("pointerdown", down);
  canvas.addEventListener("pointermove", move);
  canvas.addEventListener("pointerup", up);
  canvas.addEventListener("pointercancel", up);
  if (typeof ResizeObserver !== "undefined") { ro = new ResizeObserver(function () { size(); draw(); }); ro.observe(wrap); }
  if (typeof IntersectionObserver !== "undefined") {
    io = new IntersectionObserver(function (es) { visible = es[0].isIntersecting; if (visible) start(); else stop(); });
    io.observe(wrap);
  }
  return {
    update: update,
    destroy: function () {
      destroyed = true; stop();
      canvas.removeEventListener("pointerdown", down); canvas.removeEventListener("pointermove", move);
      canvas.removeEventListener("pointerup", up); canvas.removeEventListener("pointercancel", up);
      if (ro) ro.disconnect(); if (io) io.disconnect();
    }
  };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"accent":"#e6ff55","folds":9,"softness":0.6,"speed":1});

How it works

Each fold is a band between two sine-built curves, filled with a vertical gradient that fades to transparent at both edges and is drawn with additive blending, so overlapping folds glow like layered silk. A thin sheen line runs along each crest. Time advances the phase for the slow drift. Pressing and dragging adds to an offset that shifts the phase and height of every fold, and the release velocity carries on and then decays while the offset eases back to rest.

Accessibility and performance

Decorative canvas with a text label; pieces are hidden from assistive tech. Dragging is an extra, not a requirement. Vertical touch scrolling still works because the canvas only claims horizontal pans. Reduced motion draws one still frame and stops the float, though dragging still moves the folds on demand.

One canvas, a handful of filled paths with gradients per frame (folds times about 90 points). The pixel ratio is capped at 2 and the loop pauses off screen. Fewer folds is the cheapest lever.

SectionBackground
TechniquesPointer interaction
StackVanilla JS, Canvas
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested
On CodePenOpen the live pen on our CodePen account; fork it to experiment.
Inspired bySilk (React Bits), general effect only by React Bits on reactbits.dev. Original code, written from scratch.

Find this technique on a real site

Want to see this technique in the wild? Check any site with the free tools, then build your own version from this page.

Check which animation library a site uses · Extract a site's design tokens · Browse site breakdowns

Licence and provenance

Free to use in personal and client projects during the preview. Final licence terms will be published here before any paid tier starts; ask before reselling the code as a collection.

This is original code. The technique is a common one that we studied by measuring award-winning sites with Enrichr; no third-party code, shaders, fonts or images are used.

Rule sweep listA line sweeps across a menu row on hover or focus and exits through the opposite side. Depth parallax cardLayered shapes shift by different amounts as the pointer moves, drifting on touch screens. 3D ring carouselCards on a CSS 3D ring that rotates one step at a time, with buttons, arrow keys and autoplay.

FAQ

Is this a shader?

No, it is plain canvas 2D with layered gradients, so there is no WebGL fallback to worry about.

How do I make it stiller?

Lower the drift speed, or set it to 0 and let people drag it.

Can I change the colours?

Background and accent are parameters; the folds blend through the brand greens toward the accent.