
The slide-to-confirm button
Buttons, Physics, Transitions
NewFree
A festival pass being generated, and the ticket’s seal is not drawn, it is grown: an abelian sandpile. Grains fall one after the other on the middle of a grid, and a cell holding four topples and gives one to each neighbour, which may topple in turn, in avalanches that run out to the edge. Nobody draws the pattern: it is what thousands of avalanches leave, a square lace of triangles and stripes, and the cells reached for the first time flash. The grains dropped are the progress. If the connection drops the seal stops and goes cold until Retry warms it from the middle; at the end a light crosses it and its code is set, then Add to Wallet, and Start over drains it back into its middle.
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, viewport-fit=cover">
<title>The sandpile pass</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/wdth.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the sandpile pass: a festival pass whose seal is grown while it is generated. Grains are dropped one by one on
the middle of a grid; a cell holding four topples and gives one to each neighbour, which may topple in turn.
The avalanches build a pattern that no one drew, and the number of grains dropped is the progress. The seal is
drawn by script.js -->
<section class="sp" aria-labelledby="sp-title" data-pass>
<div class="sp__head">
<div class="sp__words">
<h2 class="sp__title" id="sp-title"><span class="sp__swap" data-title>Your festival pass</span></h2>
<p class="sp__meta"><span class="sp__swap" data-meta>Lumen Festival · 3-day pass</span></p>
</div>
<p class="sp__figure" aria-hidden="true">
<span class="sp__pct" data-pct>0%</span>
<svg class="sp__tick" viewBox="0 0 28 28" data-tick><path d="M6.5 14.8l5 5 10-11.4" pathLength="1"/></svg>
</p>
</div>
<div class="pass" data-ticket>
<div class="pass__seal" role="progressbar" aria-label="Pass seal" aria-valuemin="0" aria-valuemax="100"
aria-valuenow="0" aria-valuetext="Not started" data-bar>
<canvas class="pass__canvas" aria-hidden="true" data-seal></canvas>
<span class="pass__empty" data-empty>Seal</span>
</div>
<dl class="pass__info">
<div><dt>Holder</dt><dd>Mara Quint</dd></div>
<div><dt>Dates</dt><dd>12–14 June</dd></div>
<div><dt>Entry</dt><dd>Gate B · General</dd></div>
<div><dt>Seal</dt><dd class="pass__code" data-code>Not yet grown</dd></div>
</dl>
</div>
<div class="sp__foot">
<span class="sp__status"><span class="sp__swap" data-status>Every seal is grown once</span></span>
<button class="pill" type="button" data-btn data-autoclic>
<span class="pill__fill" aria-hidden="true"></span>
<span class="pill__label" data-label>Generate pass</span>
</button>
</div>
<p class="sr" role="status" aria-live="polite" data-live></p>
</section>
</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.
42 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 sandpile pass” from Animorio and fit it into my siteIt is Bak, Tang and Wiesenfeld’s model: a grid of cells holding grains; a cell holding four or more topples, loses four and gives one to each of its four neighbours, which may topple in turn. The grains always fall on the middle cell. A settled pile holds a little over two grains a cell on average, so its radius is about the square root of N over 2.05π: the grid is made that size plus three cells of margin, so the avalanches never reach its edge.
The pile is abelian: the order of the topplings does not change the result, so a frame’s grains are all put on the middle at once and toppled together. A cell holding v grains gives v over four to each neighbour in one go and keeps v modulo four; the cells to topple sit in a stack of integers. That is how 32,768 grains and tens of millions of topplings fit inside the frames, on a Uint32Array.
The grains dropped are N times the progress to the power EASE_IN: at 2, since the area of the pile grows with its grains, the radius of the seal grows evenly and reads as a progress bar. The progress is a clock in GSAP’s ticker with a step of at most 1/30 of a second, which pausing and a lost connection hold.
Every frame the grid becomes an ImageData of one pixel per cell, drawn on a canvas a whole number of device pixels per cell, with no smoothing, so every cell is crisp. The same pass mixes in the warm white of the cells just reached (fading in SPARK seconds), the grey of the cold seal outside the radius being warmed, the final band of light on the diagonal and the edge beyond which the pile has already drained away.
A click from a script on the pill (detail 0) plays the whole round, faster by PREVIEW: the lost connection, Retry, the pass ready, Add to Wallet and Start over, with a pretend press on the pill at every step; the waits between the steps are GSAP delayedCalls. A real click stops the round and takes over. When nothing grows and nothing glimmers the loop leaves the ticker.
| Name | File | Default | What it changes |
|---|---|---|---|
GRAINS | script.js | 32768 | How many grains grow the seal: more grains, a bigger and finer pattern. |
SECONDS | script.js | 6.5 | How many seconds the pass takes to generate in the demo. |
EASE_IN | script.js | 2 | How the grains arrive: at 2 the seal widens evenly, below it fills fast at first. |
SPARK | script.js | 0.45 | How many seconds a cell reached for the first time flashes. |
CODE | script.js | 'LMN 7F3K 2QX9' | The seal’s code, set when it has grown. |
FAIL_EVERY | script.js | 2 | One pass in how many loses the connection on the way (0 never). |
FAIL | script.js | 0.56 | Where the connection is lost, from 0 to 1. |
PREVIEW | script.js | 1.5 | In the preview’s round: how many times faster. |
LABELS | script.js | ['Generate pass', 'Pause', 'Resume', 'Retry', 'Add to Wallet', 'Start over'] | The words of the pill, state by state. |
--sand-1 | style.css | #ff5b3a | The cells holding one grain: the accent, used for the code too. |
--sand-2 | style.css | #f4f1ea | The cells holding two grains. |
--sand-3 | style.css | #2e2d31 | The cells holding three grains. |
For generating a ticket, a pass or a season ticket in an events, concerts or festival app, for a one-of-a-kind collectible, for an avatar or a pattern generated on the spot, for any wait that ends with something that is only yours.
The ticket takes the whole width less the margins, the seal stays 172 pixels wide and the details narrow beside it; the pile’s grid does not change, so the pattern is the same. The pill answers a touch as it does the mouse.
The pile grows all the same, because it is the progress, but the cells reached do not flash, the final light does not pass, the colour comes back without a wave, the words change without moving and the ticket does not nod.
GSAP 3.15.0, Bricolage Grotesque (font variabile, assi wght e wdth) 5.3.0.
Compressed with gzip, the three demo files weigh 9.9 KB; the libraries: GSAP 28 KB.
Tested on 19 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.