
The swatch fan menu
Menus, Physics, Transitions, Hover
NewFree
The menu of a treehouse camp: closed, it is a wooden trapdoor with iron hinges and a brass ring. Pressed, the trapdoor opens and a rolled rope ladder drops out, unrolling rung after rung, snapping taut and swaying before it settles: every rung is a section. The two ropes are simulated point by point, with their twist drawn; over a rung the ladder takes a weight, the rung tilts and creaks; chosen, it lights up with a warm light and the ladder climbs back, winding itself up.
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 rope ladder menu</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/standard.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the menu of a treehouse camp: closed, the button is a wooden trapdoor. Pressed, the lid swings open on its
hinges and a rope ladder drops out of the hole, unrolling rung after rung, and sways before it settles. Every
rung is a section of the site. The two ropes are simulated (verlet) and drawn on the canvas; the rungs are
real links, moved every frame to where the ropes hold them -->
<nav class="ladder" aria-label="Fernhollow Treehouses" data-ladder>
<canvas class="ladder__ropes" aria-hidden="true" data-ropes></canvas>
<ul class="ladder__rungs" id="ladder-menu" data-rungs inert>
<li><a class="rung" href="#treehouses" data-rung><span class="rung__wood" aria-hidden="true"><span class="rung__lash"></span><span class="rung__lash rung__lash--r"></span></span><span class="rung__label">Treehouses</span></a></li>
<li><a class="rung" href="#book" data-rung><span class="rung__wood" aria-hidden="true"><span class="rung__lash"></span><span class="rung__lash rung__lash--r"></span></span><span class="rung__label">Book a stay</span></a></li>
<li><a class="rung" href="#trails" data-rung><span class="rung__wood" aria-hidden="true"><span class="rung__lash"></span><span class="rung__lash rung__lash--r"></span></span><span class="rung__label">Forest trails</span></a></li>
<li><a class="rung" href="#campfire" data-rung><span class="rung__wood" aria-hidden="true"><span class="rung__lash"></span><span class="rung__lash rung__lash--r"></span></span><span class="rung__label">Campfire nights</span></a></li>
<li><a class="rung" href="#find-us" data-rung><span class="rung__wood" aria-hidden="true"><span class="rung__lash"></span><span class="rung__lash rung__lash--r"></span></span><span class="rung__label">Find us</span></a></li>
</ul>
<!-- the trapdoor is the menu button -->
<button class="hatch" type="button" aria-expanded="false" aria-controls="ladder-menu" data-hatch data-autoclic>
<span class="hatch__hole" aria-hidden="true">
<span class="hatch__close">
<svg viewBox="0 0 16 16"><path d="M4 4l8 8M12 4l-8 8"/></svg>
Close
</span>
</span>
<span class="hatch__knuckle" aria-hidden="true"></span>
<span class="hatch__knuckle hatch__knuckle--low" aria-hidden="true"></span>
<span class="lid" aria-hidden="true" data-lid>
<span class="lid__face lid__face--front">
<span class="lid__strap"></span>
<span class="lid__strap lid__strap--low"></span>
<span class="lid__word">Menu</span>
<svg class="lid__ring" viewBox="0 0 30 30">
<rect class="lid__plate" x="9" y="2" width="12" height="7" rx="2"/>
<circle class="lid__loop" cx="15" cy="17" r="8"/>
<path class="lid__glint" d="M9.2 13.4a7 7 0 0 1 4.6-3.6"/>
</svg>
</span>
<span class="lid__face lid__face--back"></span>
</span>
<span class="sr" data-name>Open the menu</span>
</button>
<p class="sr" aria-live="polite" data-live></p>
</nav>
</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.
29 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 rope ladder menu” from Animorio and fit it into my siteEach rope is a row of points ten pixels apart, moved with Verlet integration: the speed lives in the difference between the position now and the one before, with gravity and damping (DAMPING). At every step, a hundred and twenty a second and never longer than 1/30 of a second, fourteen rounds of constraints put every link back to its length and the first point of each rope back on the trapdoor.
Each rung ties a point of the left rope to the one at the same height on the right rope with a fixed distance constraint: so the ladder skews, swings and twists as one. The rungs are real links in a list: every frame the script puts each one at the midpoint of its two points and turns it by the angle between them (atan2).
The part of the rope still rolled up is not free: its points sit on the roll, which falls with gravity and a little drag, and they join the physics one after the other, with the roll's speed, as it goes down. The rungs flip out (scaleY), the roll gets thinner; when the last point is free the ropes snap taut, the trapdoor takes the jolt and the ladder gets a sideways push (SWAY) that sets it swinging. Climbing back is the reverse: the roll sits on the rope and eats one point at a time, at REEL pixels a second.
Over a rung its two points get a downward force (WEIGHT), stronger on the side of the pointer. The ropes above stretch a little (STRETCH) and, on the side of the pointer, the link just above the rung lengthens while the one below shortens by as much: so only that rung tilts and the rest of the ladder hangs straight under it. The rung shivers for a moment (a damped sine on its angle) and a pinch of sawdust falls from the lashing.
The ropes are smooth curves (quadraticCurveTo through the midpoints) on a canvas: a dark edge, the body with a gradient from above, a line of light, and the strands of the twist as slanted strokes at a fixed pitch along the rope. Where a link is stretched the pitch grows and the strokes draw out. The cone of light from the trapdoor and the glow of the lit rung are radial gradients; when nothing moves any more the canvas stops redrawing.
The trapdoor is a button with aria-expanded and aria-controls; the lid turns in 3D on its hinge with GSAP (a bounce opening, a knock closing). The list of rungs is inert while the ladder is rolled up. From the keyboard the focus goes to the rung of the current section when the ladder lands; the arrows go through the rungs, Enter chooses, Escape and a click outside wind the ladder back up; an aria-live area reads out the chosen section.
| Name | File | Default | What it changes |
|---|---|---|---|
GRAVITY | script.js | 1900 | Gravity on the ropes, in pixels a second squared. |
DAMPING | script.js | 0.9 | How quickly the swing dies away (0.2 it swings for long, 3 it stops almost at once). |
SWAY | script.js | 110 | The sideways push the ladder gets as it lands, in pixels a second. |
WEIGHT | script.js | 1500 | How hard a rung under the pointer is pulled down, as if someone stood on it. |
STRETCH | script.js | 0.05 | How much the ropes above that rung give under the weight (0.05 = five per cent); it also sets how far the rung tilts. |
RUNG_GAP | script.js | 70 | The rope between one rung and the next, in pixels. |
GLOW | script.js | 0.6 | The seconds a chosen rung stays lit before the ladder climbs back. |
REEL | script.js | 700 | How fast the ladder winds itself back up, in pixels a second. |
For the menu of an outdoor or playful site: a campsite, a mountain hut, treehouses, an adventure park, a kids' activity, a game. From three to six short sections (the name has to fit on a rung 176 pixels wide); with more the ladder gets too long for the screen.
The ladder is 340 pixels wide and fits whole at 390, open and while it swings. A finger pressing a rung stands on it (the rung tilts towards the finger and creaks) and letting go chooses it; the trapdoor opens and closes with a tap.
Nothing falls and nothing swings: the lid fades away and the ladder appears already hanging straight, fading in; over a rung the wood warms in colour without moving; the chosen rung lights up, then the ladder fades out and the lid comes back.
GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 13 KB; the libraries: GSAP 28 KB.
Tested on 9 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.