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.
| Section | Background |
|---|---|
| Techniques | Pointer interaction |
| Stack | Vanilla JS, Canvas |
| Works with | React, Next.js, Vanilla |
| Dependencies | None |
| Status | tested |
| On CodePen | Open the live pen on our CodePen account; fork it to experiment. |
| Inspired by | Silk (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.
More elements
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.