
The fluid menu
Menus, WebGL, Physics, Transitions
NewFree
A picture house's menu stands on a little stage behind a red velvet curtain: pressing Menu the two drapes run along the rail, are tied back at the sides with their folds swinging, and under a spot the entries appear. The curtain is a truly simulated cloth, points and threads in WebGL, and with the pointer near it its edge parts and ripples.
A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The curtain menu</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bodoni-moda@5.3.0/opsz.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bodoni-moda@5.3.0/opsz-italic.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/hanken-grotesk@5.3.0/index.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the curtain menu: the button that opens and closes it, and the stage in its gilded arch, closed by the
curtain, with the menu standing behind it -->
<div class="theatre" data-theatre>
<div class="bar">
<button class="toggle" type="button" aria-expanded="false" aria-controls="programme" data-toggle data-autoclic>
<svg class="toggle__icon" viewBox="0 0 24 24" aria-hidden="true">
<path class="toggle__rail" d="M2.5 3.5h19"/>
<path class="toggle__drape toggle__drape--left" d="M3.5 4h8.2c-.4 5.6-2.6 11.3-8.2 16.2z"/>
<path class="toggle__drape toggle__drape--right" d="M20.5 4h-8.2c.4 5.6 2.6 11.3 8.2 16.2z"/>
</svg>
<span class="toggle__words" aria-hidden="true"><span class="toggle__word" data-word>Menu</span></span>
<span class="sr" data-name>Open the menu</span>
</button>
</div>
<!-- the stage: the menu stands on it, behind the curtains. The curtains and the valance over them are a cloth
simulated in WebGL on the canvas, which lets the pointer through to the menu once they are open -->
<div class="frame">
<div class="scene" data-stage>
<div class="set" aria-hidden="true">
<span class="set__spot" data-spot></span>
<span class="set__floor"><svg viewBox="0 0 100 100" preserveAspectRatio="none"><path d="M6.8 0L-2.8 100M10.4 0L1.6 100M14.0 0L6.0 100M17.6 0L10.4 100M21.2 0L14.8 100M24.8 0L19.2 100M28.4 0L23.6 100M32.0 0L28.0 100M35.6 0L32.4 100M39.2 0L36.8 100M42.8 0L41.2 100M46.4 0L45.6 100M50.0 0L50.0 100M53.6 0L54.4 100M57.2 0L58.8 100M60.8 0L63.2 100M64.4 0L67.6 100M68.0 0L72.0 100M71.6 0L76.4 100M75.2 0L80.8 100M78.8 0L85.2 100M82.4 0L89.6 100M86.0 0L94.0 100M89.6 0L98.4 100M93.2 0L102.8 100"/></svg></span>
</div>
<nav class="programme" id="programme" aria-label="Main" data-menu inert>
<ul class="programme__list">
<li><a class="act" href="#" data-link><span class="act__name">What’s on</span><span class="act__note">Eighteen nights in October</span></a></li>
<li><a class="act" href="#" data-link><span class="act__name">Films</span><span class="act__note">On 35 and 70 mm</span></a></li>
<li><a class="act" href="#" data-link><span class="act__name">Supper club</span><span class="act__note">Tables from 6 pm</span></a></li>
<li><a class="act" href="#" data-link><span class="act__name">Private hire</span><span class="act__note">Up to 240 guests</span></a></li>
<li><a class="act" href="#" data-link><span class="act__name">Visit</span><span class="act__note">Box office and directions</span></a></li>
</ul>
<div class="programme__foot" data-extra>
<a class="tickets" href="#">Book tickets</a>
</div>
</nav>
<canvas class="cloth" data-cloth aria-hidden="true"></canvas>
<div class="velvet" data-velvet aria-hidden="true"><span></span><span></span></div>
<div class="apron" data-apron aria-hidden="true"></div>
</div>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="script.js"></script>
</body>
</html>Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.
30 values to change
With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:
Take “The curtain menu” from Animorio and fit it into my siteEach drape is a grid of 40 by 34 points. On every step (fixed, a sixtieth of a second) a point moves as far as it moved in the step before, lightly damped, plus gravity: this is Verlet integration, which keeps no speed but the previous position. Then the threads between neighbouring points (across, down, diagonally, and two apart downwards so it does not crease too much) are pulled back to their length STIFF times in a row. A thread resists stretching a lot and squeezing hardly at all: so, gathered, the velvet folds softly instead of crumpling.
The top row hangs from the hooks of the rail, which go in and out in depth (0, 1, 0, −1, with small differences from one pleat to the next): the threads across are born FULLNESS times longer than the gap between two hooks, and that extra cloth is what makes the pleats. Opening, the hooks close up to GATHER of the stage width and the pleats deepen by themselves, because the thread stays as long as it was.
While the curtain opens, a row halfway down is pulled a little on every iteration towards a point that starts at the hooks and travels to the knot, at TIE (across and down): pulled to the knot at once, the cloth below would flip over it. The opening itself (from 0 to 1) advances inside the simulation, one step at a time: on a slow phone that runs fewer steps than it should, the cord never runs ahead of the cloth.
The drapes are two meshes of triangles with normals worked out again every frame from the neighbouring points. The shader gives the velvet its look: diffuse light, a sheen where the fold turns edge-on (1 − n.z), darkness deep in the folds according to the depth of the point, and a touch of noise. At the bottom the fringe: twisted gold strands drawn with a sine on the cloth coordinate, with ragged ends made by discarding pixels; above it, a braid. The pelmet is the same shader on a still, swagged shape, with the rosettes drawn in the shader.
Points within a sixth of the stage from the pointer are pushed aside (above all along the free edge, which so parts), dragged by its movement and billowed forwards, with a ripple that goes on even with the pointer still. At rest a light draught (BREEZE) moves the velvet near the bottom. The canvas lets the pointer through: underneath is the real menu, with its links.
| Name | File | Default | What it changes |
|---|---|---|---|
OPEN | script.js | 1.8 | The seconds of the curtain opening (closing takes a little less). |
GATHER | script.js | 0.3 | Where the top of each open drape stops, as a fraction of the stage width from its side. |
TIE | script.js | [0.12, 0.52] | Where each drape is tied back: across and down, as fractions of the stage. |
FULLNESS | script.js | 1.8 | How much wider the velvet is than the half of the stage it covers: more, deeper folds. |
SWAY | script.js | 1 | How much the curtain moves aside and sways near the pointer. |
BREEZE | script.js | 0.6 | The draught that keeps the velvet moving at rest. |
STIFF | script.js | 10 | How many times a step the threads are pulled back to their length: fewer, a stretchier cloth (and lighter to work out). |
For the site of a theatre, a cinema, a club, a restaurant with a dining room or an event: wherever opening the menu can be a small show and velvet truly belongs. For an everyday site it is too much: the curtain asks for a moment’s attention.
The stage narrows to the width of the screen and becomes tall; there the drapes gather tighter (70% of GATHER and TIE), with less cloth and the knot a little lower, to leave the menu clear. A tap opens and closes; a finger moving over the curtain makes it sway like the mouse.
The curtain does not run: it fades and comes back already open and tied (or already shut), with still folds, no draught and no swaying under the pointer; the entries fade in without rising.
GSAP 3.15.0, Bodoni Moda (font variabile) 5.3.0, Hanken Grotesk (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 14 KB; the libraries: GSAP 28 KB.
Tested on 4 October 2026
Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.