
The layered button
Buttons, Hover, Physics
Free
The Log out button of an account on a black page, lit by a candle burning in the round end of the pill. The flame is a shape drawn every frame on a canvas: it breathes, flickers at the tip and leans on a spring towards the pointer coming near, as if in a breath of air; its warm light falls on the pill, the dish and the words. At a click a brass snuffer comes down, the flame dies under the bell, a thread of smoke rises from the wick curling in curl noise and the pill goes cold and dark: Signed out. The second click strikes a match, brings it to the wick and the light comes back: Welcome back, Noor.
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 candle button</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/wdth.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the candle button: the Log out button of an account, with a lit candle in the round left end of the pill, on a
black page. The flame breathes and leans to the pointer coming near, and its warm light falls on the pill. A
click brings down a brass snuffer, the flame dies under it, a thread of smoke curls up from the wick and the
pill goes dark; another click strikes a match that lights the wick again -->
<div class="candle" data-candle>
<button class="candle__btn" type="button" data-btn data-autoclic>
<span class="candle__fill" aria-hidden="true"></span>
<canvas class="candle__art" aria-hidden="true" data-art></canvas>
<span class="candle__words">
<span class="candle__word" data-word>Log out</span>
</span>
</button>
<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.
30 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 candle button” from Animorio and fit it into my siteThe flame is a polygon built on a spine of 18 points: from foot to tip the spine moves by (lean · s² + flick · s³) · height, so it bends more and more towards the tip; the width is (1 − s)^1.25 for the tapered tip and a root near the foot for the round bottom. The same shape is filled four times in lighter mode: a wide soft bloom with shadowBlur, the body with a vertical gradient from blue to orange, the pale heart shorter and narrower, the blue cup at the foot. Never a glowing round spot: even the bloom has the flame's shape.
The height follows a slow wave of BREATH a second plus two octaves of gradient noise, and the tip flickers with fast noise. The lean is a damped spring (1.6 swings a second, damping 0.32) towards a target: the pointer near pulls the flame towards itself with a force that falls with the cube of the distance (REACH is where it is half), and its horizontal speed adds the breath of the air it pushes. So the flame goes a little past and comes back, like a real one.
The wick gives off seventy particles a second, in order. Each rises by its warmth and is carried by the curl of a two-dimensional gradient noise field that drifts in time: the curl, worked out by finite differences, is a flow without divergence, so the air turns and curls without piling up. The ribbon is drawn by joining consecutive particles in runs of four, each run a single path of quadratic curves: thin and pale near the wick, wider and fainter as it rises, fading towards the edges of the canvas.
They are two GSAP timelines on the same objects the loop draws. The snuffer comes down along its rod with back.out, the flame chokes (height and width shrink), the bell lifts with power2.in and the smoke starts as it rises, with the ember fading. The match fades in from the edge, flares (1.55 times its flame, then 1), comes down in an arc, and the wick catches with elastic.out; its flame always stands upright whatever the stick's angle and leans back against its motion.
Every frame the light follows the flame (plus the match's) with a small lag and its flicker, and is written into three CSS variables: --glow sets the opacity of the warm gradients over the pill and of the halo, --out that of the cold dark coat, --lit mixes the colour of the words with color-mix. The dish, the wax and the glow through the wax use the same value, so the whole scene warms and cools together.
A real button with aria-disabled while it goes out or lights, an aria-live region saying Logging out…, Signed out., Signing in… and Signed in. Welcome back, Noor. The pill is as wide as the longest of the six phrases, measured already wide. The lit flame always moves, so the loop runs while it is lit and in view; in the dark, once smoke, snuffer and match are done, it stops.
| Name | File | Default | What it changes |
|---|---|---|---|
LOG_OUT | script.js | Log out | The words while signed in. |
LOGGING_OUT | script.js | Logging out… | The words while the candle is put out; the aria-live region says it too. |
SIGNED_OUT | script.js | Signed out | The words once the candle is out. |
SIGN_IN | script.js | Sign in | The words in the dark, offering the way back. |
SIGNING_IN | script.js | Signing in… | The words while the match lights the candle. |
SIGNED_IN | script.js | Welcome back, Noor | The welcome. The name is made up: in a real site it is the user's. |
SAID | script.js | 1 | The seconds Signed out or the welcome stays before the button offers the way back. |
SNUFF | script.js | 0.7 | The seconds the snuffer takes to come down on the flame. |
SMOKE | script.js | 3 | How many seconds the wick keeps smoking. |
CURL | script.js | 1 | How much the air curls the smoke: 0 it rises straight, 3 it twists a lot. |
BREATH | script.js | 0.35 | The flame's slow breaths a second. |
LEAN | script.js | 1 | How far the flame leans to the pointer: 0 it does not feel it. |
REACH | script.js | 170 | How far away the pointer is felt, in pixels: at this distance the flame leans half as much. |
For signing out of an account, or in, on a site with a warm, night-time tone: a restaurant, a wine bar, a candle or perfume brand, a journaling or meditation app, a fiction or role-playing site; also as a switch for the dark theme. The sign out has to be wired to your own: here it is all simulated.
The pill is about 250 pixels wide and fits at 390 with room to spare; the smoke, the snuffer and the match stay above the candle and left of the words. A finger resting near the candle bends the flame; a tap puts it out or lights it again.
The flame stands still, with no breath or flicker, and does not lean. At a click it fades in half a second and the light on the pill fades out, with no snuffer or smoke; at the second click it fades back, with no match. The words change with a fade; the pill does not swell and the words do not widen.
GSAP 3.15.0, Mona Sans (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 13 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.