
The layered button
Buttons, Hover, Physics
Free
A dark button with two beams of light running round its border: when the pointer comes close the beams slide towards it on a spring and bow off the border to reach it, and inside a soft light turns on. Pressed, a wave of light runs both ways round the border and the beams speed up until the workspace is ready.
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 light beam button</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/geist@5.3.0/index.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<div class="cta" data-cta>
<!-- the button: a beam of light runs round its border and swings to the pointer; inside, a soft light under the
pointer with dust drifting through it. The border, the light and the dust are drawn by the script -->
<div class="slot" data-slot>
<button class="beam" type="button" aria-describedby="cta-note" data-btn>
<span class="beam__face" aria-hidden="true">
<canvas class="beam__inside" data-inside></canvas>
</span>
<span class="beam__labels">
<span class="beam__label" data-label="idle">Get started<svg class="beam__arrow" viewBox="0 0 16 16" aria-hidden="true"><path class="beam__stem" d="M2.5 8h9"/><path class="beam__tip" d="M7.5 3.5 12 8l-4.5 4.5"/></svg></span>
<span class="beam__label" data-label="busy" aria-hidden="true">Creating workspace…</span>
<span class="beam__label" data-label="done" aria-hidden="true"><svg class="beam__check" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8.4 6.4 11.6 13 4.6" pathLength="1" data-check/></svg>You're in</span>
</span>
<canvas class="beam__bloom" data-bloom aria-hidden="true"></canvas>
<canvas class="beam__rim" data-rim aria-hidden="true"></canvas>
</button>
</div>
<!-- the line under the button: empty at rest, it says what the button is doing -->
<p class="cta__note" id="cta-note"><span data-note></span></p>
<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.
26 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 light beam button” from Animorio and fit it into my siteThe border is a rounded rectangle walked by length: a function gives the point s pixels from the top left corner, going through the four sides and the four quarter circles. A beam is just a number on that loop and a speed. It is drawn on a canvas as a run of dashes 2.5 pixels long, each with its own colour and opacity: the white head, then the --tint colour, then --tail fading to nothing, and under it a second wide, faint stroke that gives the tail a body. A copy of the canvas, blurred by the CSS and set to screen, is the bloom.
From the pointer two points of the border are found: above and below it while it is over the long sides, and mirrored round the rounded end once it is past it. The same two points inside the button and outside, so nothing jumps when the pointer crosses the edge. When the hold begins each beam takes the nearer point (the pair swaps if that is a shorter way) and keeps it. Every frame the acceleration is a blend between the cruise speed and a spring of strength SWING towards its point, weighted by how close the pointer is; the step is capped at 1/30 of a second.
While the pointer holds the beams, the points near the head move towards it by up to BEND pixels, less and less along the tail (squared): the light leaves the border and leans out. The bloom also gets an extra glow pushed from the head towards the pointer.
A second canvas, clipped to the button’s shape, has a radial gradient chasing the pointer. On the same canvas DUST specks of dust can wander (none by default), held back by the air and pushed by the pointer moving through them: each one is lit only by the light that reaches it, a gaussian round the pointer and one round each head, and out of the light it does not show.
A press sends a wave from the pressed point: two fronts going opposite ways for half the loop with power2.out, leaving a faint ring behind them, with a flash where they meet. Then a delayedCall does the pretend work: the beams let go of the pointer and their cruise rises RUSH times, the label changes sliding and blurring, aria-busy is true and an aria-live region says so. At the end a green wave, the tick with its stroke-dashoffset and the way back; a click during the work cancels.
| Name | File | Default | What it changes |
|---|---|---|---|
LAP | script.js | 3.4 | The seconds a beam takes to go once round the border, at rest. |
BEAMS | script.js | 2 | How many beams run round the border. |
TAIL | script.js | 0.4 | How long a beam is, as a share of the border. |
SWING | script.js | [150, 13] | The spring that takes the beams to the pointer: how stiff, and how much it holds them back. |
REACH | script.js | 90 | How many pixels from the button the pointer starts to pull the beams. |
BEND | script.js | 9 | How many pixels the light bows off the border towards the pointer. |
RUSH | script.js | 4 | How many times faster the beams run while the workspace is being made. |
DUST | script.js | 0 | How many specks of dust drift through the light inside the button: none by default. |
WORK | script.js | 2.4 | The seconds the pretend sign-up takes. |
DONE | script.js | 2.8 | The seconds the button says You’re in before it goes back. |
For the main action of a dark landing page: sign up, start for free, get the app. It wants a dark, calm background and only one button like this per page; next to a plain secondary button it stands out even more.
The button and the line below fit a 390 pixel screen. Without a pointer coming close the beams go round by themselves; a touch lights the inside where the finger lands for a moment, and the wave leaves from right there.
The beams stay still in their place and the button does not shrink. The light inside still sits under the pointer; a press lights the whole border at once and fades it instead of sending the wave, and the labels change with a fade.
GSAP 3.15.0, Geist (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 11 KB; the libraries: GSAP 28 KB.
Tested on 12 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.