
The slide-to-confirm button
Buttons, Physics, Transitions
NewFree
The switch between the light and the dark theme made like the chain of a pendant lamp: a chain of long brass links, simulated link by link, with a drop shaped pull at the end. Pull it down with the mouse or a finger, the chain slides a little out of the socket, clicks, and the lamp goes on or off: the light falls in a cone on the words Lights on and the little room round it changes from its light colours to its dark ones. Let go, the chain swings like a pendulum and comes to rest.
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 pull chain button</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/index.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/index.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the pull chain button: a pendant lamp with a chain of long metal links and a brass drop at the end. Pull the
chain (or press the drop, or Enter) and the lamp clicks on or off, and the little room it hangs in switches
between its light and its dark colours. Let go, and the chain swings like a pendulum and settles -->
<div class="room" data-room data-light="on">
<div class="room__scene" data-scene>
<!-- the light: a cone from the shade down to the floor, painted once on its own canvas and blended into the
room; it swings with the shade -->
<canvas class="room__light" aria-hidden="true" data-cone></canvas>
<!-- the lamp: the cord, the canopy, the enamel shade and the bulb, swinging round the top of the cord -->
<svg class="room__lamp" viewBox="0 0 360 500" aria-hidden="true" data-lamp>
<defs>
<linearGradient id="chain-enamel" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" style="stop-color: var(--enamel-dark)"/>
<stop offset="0.28" style="stop-color: var(--enamel)"/>
<stop offset="0.42" style="stop-color: var(--enamel-light)"/>
<stop offset="0.6" style="stop-color: var(--enamel)"/>
<stop offset="1" style="stop-color: var(--enamel-dark)"/>
</linearGradient>
<linearGradient id="chain-brass" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" style="stop-color: var(--brass-dark)"/>
<stop offset="0.38" style="stop-color: var(--brass-light)"/>
<stop offset="0.62" style="stop-color: var(--brass)"/>
<stop offset="1" style="stop-color: var(--brass-dark)"/>
</linearGradient>
<radialGradient id="chain-bulb" cx="0.5" cy="0.35" r="0.65">
<stop offset="0" style="stop-color: var(--bulb-core)"/>
<stop offset="1" style="stop-color: var(--bulb)"/>
</radialGradient>
</defs>
<g data-swing>
<path class="lamp__cord" d="M180 -4 V92"/>
<path d="M168 88 h24 l-3 10 h-18 z" fill="url(#chain-brass)"/>
<!-- the bulb, half out of the shade -->
<path class="lamp__bulb" d="M166 160 C164 176 170 192 180 192 C190 192 196 176 194 160 Z" fill="url(#chain-bulb)" data-bulb/>
<path class="lamp__filament" d="M174 170 q3 6 6 0 q3 6 6 0" data-filament/>
<!-- the shade: a dome of enamel with a rolled rim, and the inside showing under it -->
<ellipse class="lamp__inside" cx="180" cy="168" rx="74" ry="9" data-inside/>
<path d="M180 98 C150 98 140 112 132 132 C124 150 112 160 104 166 C108 172 252 172 256 166 C248 160 236 150 228 132 C220 112 210 98 180 98 Z" fill="url(#chain-enamel)"/>
<path class="lamp__sheen" d="M156 108 C146 118 140 134 132 150"/>
<path d="M104 166 C108 173 252 173 256 166" class="lamp__rim"/>
<!-- the socket the chain comes out of -->
<rect x="203" y="166" width="10" height="12" rx="2" fill="url(#chain-brass)"/>
</g>
</svg>
<!-- the chain and the drop are drawn link by link on the canvas -->
<canvas class="room__chain" aria-hidden="true" data-chain></canvas>
<!-- what the light falls on -->
<p class="room__label" aria-hidden="true">
<span class="room__word" data-word="on">Lights on</span>
<span class="room__word" data-word="off">Lights off</span>
</p>
<p class="room__hint" data-hint>Pull the chain to dim the room</p>
<!-- the real switch: a button over the brass drop, moved with it every frame -->
<button class="pull" type="button" role="switch" aria-checked="true" aria-label="Lights" data-pull data-autoclic></button>
</div>
<p class="sr" aria-live="polite" data-live></p>
</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.
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 pull chain button” from Animorio and fit it into my siteThe chain is a row of points: the first is in the socket, then one every LINK pixels, and the last two are the drop's ring and its tip, heavier than the links. At every step of 1/120 of a second each point moves by its speed (the distance from where it was before, damped by DAMP) plus gravity; then, fourteen times, each pair of neighbouring points is brought back to its distance, the lighter point moving more. Time is gathered up and spent in fixed steps, never more than 1/30 of a second per frame.
The hand holds the drop's ring, but never further than the chain can reach. When the point it wants is further than the chain's length, what is left over becomes the travel in the socket, harder and harder (TRAVEL times 1 minus e to the minus the excess over TRAVEL): the chain is taut and slides out. At CLICK of the travel the switch clicks once only and is ready again when the chain has gone three quarters of the way back; let go, a damped spring pulls the chain back in and the chain, set off with the hand's speed, swings.
Each link is drawn on the segment between two points, turned along it and longer than the step, because the links hook into each other: the even ones face on, a loop of wire with its dark edge, the metal and a thread of light on the upper side; the odd ones edge on, a thin bar. Above the first point the links still inside the socket are drawn too, hidden by a clip until they come out, so pulling the chain you see it come out. The drop is a drop shaped path with a brass gradient, a turned groove and the highlight.
The cone, the glow round the bulb and the pool on the floor are painted once on a canvas, their edges softened by the canvas shadow (the shape drawn out of sight and only its shadow in view), and the canvas is blended into the wall with mix-blend-mode: screen: it lifts what it lights instead of covering it. Switching on, the cone flickers; switching off, the filament stays orange for a moment; the room's colours change with a state attribute and a transition, and the chain goes from its lit tones to its dark ones with a value from 0 to 1.
The shade is a pendulum on its cord: the taut chain pulls it sideways as far as it has come out, and it turns round the top of the cord with the cone and the socket, so the first point of the chain moves with it. When nothing has moved for three quarters of a second the simulation stops and the canvas is not redrawn; the hand, the mouse near the chain or a change of light wake it.
| Name | File | Default | What it changes |
|---|---|---|---|
LINKS | script.js | 15 | How many links the chain has. |
LINK | script.js | 9.6 | The length of one link, in pixels of the drawing. |
GRAVITY | script.js | 2600 | How hard the chain is pulled down, in pixels per second squared: more, it swings faster. |
DAMP | script.js | 0.6 | How quickly the swing dies away (0 = it swings for a long time). |
TRAVEL | script.js | 18 | How far the chain can be pulled out of the socket, in pixels. |
CLICK | script.js | 0.72 | How much of that travel it takes to click the switch. |
SWAY | script.js | 1 | How much the shade itself swings when the chain is tugged (0 = it keeps still). |
START_ON | script.js | true | Whether the lamp starts switched on (light theme) or off (dark theme). |
For the theme switch of a site with some character: a blog, a portfolio, the page of a café or a lamp shop, a documentation page that wants a touch of play. It works in a corner of the header, scaled down, or large on a settings page. Here it changes the colours inside its own frame only: on your site setLight just needs to set the attribute on the document instead of on the room.
The little room narrows to the width of the screen and the chain with it. A finger takes the drop and pulls it like the mouse; a short tap gives the tug by itself. The real button stays still over the drop at rest and is wide enough for a thumb, even while the chain swings.
No swinging: the chain hangs still and straight, the shade does not sway, and a tap, Enter or Space switch on and off with no tug, the light and the colours fading and no flicker. Dragged with a finger or the mouse the chain still pulls, and let go it hangs straight again without swinging.
GSAP 3.15.0, Fraunces (font variabile) 5.3.0, Instrument Sans (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 11 KB; the libraries: GSAP 28 KB.
Tested on 5 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.