Skip to content
animorio
IT
All components

The light beam button

Buttons, Hover, PhysicsGSAP, CanvasAdvancedCompressed weight: Demo 11 KBGSAP 28 KBFreeDownload the zip

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.

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 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&hellip;</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>

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.

26 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 light beam button” from Animorio and fit it into my site

What it does

On a black background a dark glass button with rounded corners: Get started with an arrow. Two beams of light go round the border, each with a white head and a tail fading from lilac to sky blue. When the pointer comes within 90 pixels, the two beams leave their lap and slide on a spring until they hold it like calipers, one above and one below, or both round the rounded end if the pointer is past the side; their light bows off the border towards the pointer. Inside the button a soft light turns on under the pointer; the arrow grows its stem. Pressing, a wave of light leaves the pressed point, runs both ways round the border and meets on the other side with a flash; the beams let go of the pointer and race four times faster, the label turns into Creating workspace… and under the button appears the line This takes a few seconds. Click again to cancel. After a couple of seconds a green wave, the border turns green and a tick draws itself: You’re in, and the line below says We sent a sign-in link to your inbox. Shortly after everything goes back and the line below disappears. Clicking while it works, the light dims for a moment and for a couple of seconds the line says Cancelled. Nothing was created.

How it works

The border measured by length

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

The calipers and the spring

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.

The light that bends

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.

The light inside

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.

The wave and the states

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.

Values to change

NameFileDefaultWhat it changes
LAPscript.js3.4The seconds a beam takes to go once round the border, at rest.
BEAMSscript.js2How many beams run round the border.
TAILscript.js0.4How long a beam is, as a share of the border.
SWINGscript.js[150, 13]The spring that takes the beams to the pointer: how stiff, and how much it holds them back.
REACHscript.js90How many pixels from the button the pointer starts to pull the beams.
BENDscript.js9How many pixels the light bows off the border towards the pointer.
RUSHscript.js4How many times faster the beams run while the workspace is being made.
DUSTscript.js0How many specks of dust drift through the light inside the button: none by default.
WORKscript.js2.4The seconds the pretend sign-up takes.
DONEscript.js2.8The seconds the button says You’re in before it goes back.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • Finding the border point by the angle from the middle worked inside the button but not outside, and the nearest point worked outside but not inside: crossing the edge the beams jumped to the other side. The calipers (above and below, mirrored on the rounded ends) give the same points inside and outside.
  • With two beams chasing the same point, the second one landed on the first and only one showed; a beam choosing the side it arrived from left one behind. Now the pair of points is handed out once at the start of the hold, the way that costs the shortest path.
  • The sizes are taken with offsetWidth, not getBoundingClientRect: the button shrinks when pressed, and measured while small it would have redrawn the border smaller than it is.
  • The canvases are drawn at double density and their size in CSS pixels is written by hand; without that, on a phone the thin beams looked blurred.
  • The beams’ colours come from the CSS variables, read with a spare 2D context (fillStyle rewrites any colour as hex or rgba) and read again every so often: so they also change live in the Playground.

Libraries

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.

Testing

Tested on 12 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