Skip to content
animorio
IT
All components

The pull chain button

Buttons, Physics, TransitionsCanvas, GSAPAdvancedCompressed weight: Demo 11 KBGSAP 28 KBFreeDownload the zip

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.

The code

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>

Playground

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

Or ask Claude Code for it

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 site

What it does

On a white page there is a small linen coloured room with round corners, with a pendant lamp hanging from a black cord: a bottle green enamel shade with a rolled rim, the white of its inside showing underneath and the bulb peeping out. From the brass socket hangs a chain of long links, one seen face on and the next edge on as in real chains, with a brass drop at the end. The light comes down from the shade in a soft cone and makes a pool on the floor, where it says Lights on and below Pull the chain to dim the room; the chain throws its shadow on the wall. Moving the mouse past the chain pushes it aside as a hand would, and over the drop the drop brightens and gives a small start. Take the drop and pull: the chain goes straight, then slides a few pixels out of the socket against a resistance that grows, and three quarters of the way down it clicks: the lamp goes off, the cone fades, the filament stays orange for a moment and the room turns almost black, with the words Lights off in pale ink and the hint Pull the chain to light the room. The shade sways a little on its cord. Let go, the chain goes back into the socket, swings like a pendulum and comes to rest. Pulled again, the bulb comes back on with a small flicker, like an incandescent bulb. A tap on the drop, Enter or Space give the tug by themselves.

How it works

A Verlet chain

The 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.

Pulling and clicking

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.

Real links on the canvas

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 light and the room

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, and rest

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.

Values to change

NameFileDefaultWhat it changes
LINKSscript.js15How many links the chain has.
LINKscript.js9.6The length of one link, in pixels of the drawing.
GRAVITYscript.js2600How hard the chain is pulled down, in pixels per second squared: more, it swings faster.
DAMPscript.js0.6How quickly the swing dies away (0 = it swings for a long time).
TRAVELscript.js18How far the chain can be pulled out of the socket, in pixels.
CLICKscript.js0.72How much of that travel it takes to click the switch.
SWAYscript.js1How much the shade itself swings when the chain is tugged (0 = it keeps still).
START_ONscript.jstrueWhether the lamp starts switched on (light theme) or off (dark theme).

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • The real button, attached to the swinging drop, was a moving target: hard to catch with a finger and never still for an automated test. Now it stays still over the drop at rest, the hand takes the drop wherever it is and the focus ring is drawn on the canvas round the drop.
  • With the hand holding the drop and the socket fixed, pulling beyond the chain's length stretched the links like rubber bands: the excess becomes the travel in the socket, with a resistance that grows, and the hand never goes further than the chain can reach.
  • A fast hand moved the last point by many pixels in one step and the chain tore apart for a frame: the held point goes 60% of the way to the hand at each round of the constraints.
  • The cone of light with an SVG blur filter, blended into the wall, was worked out again at every frame of the chain and slowed everything down: now it is painted once on a canvas of its own, which turns with the shade by a CSS transform.
  • The canvas blur filter does not exist in Safari: the edges of the cone are the canvas shadow of a shape drawn out of sight, which works everywhere.
  • The edge on and face on links swapped over while the chain came out of the socket: the parity of each link counts the ones still inside too, so the chain slides without changing.

Libraries

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.

Testing

Tested on 5 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: Done

License

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.

All components