
The cookie banner
Consent, Transitions, SVG, Physics
NewFree
A studio's invoices in a real table: the rows picked are held by one elastic shape that grows out of the box ticked, stretches over the next row, pours down the whole table and tears in two when a row in the middle is let go. The footer rolls out into the bar of actions; deleted rows fall through a trapdoor in the table while the others slide over it, and Undo swings them back up.
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 trapdoor table</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/archivo@5.3.0/standard.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the trapdoor table: a studio's invoices. The rows picked are held by one elastic shape that grows out of the
box ticked; the footer rolls out into a bar of actions; deleted rows fall through a trapdoor in the table
while the rows below slide over the hole, and Undo swings them back up -->
<section class="ledger" aria-labelledby="ledger-title" data-ledger data-autoclic>
<header class="ledger__head">
<h2 class="ledger__title" id="ledger-title">Invoices</h2>
<p class="ledger__note">September and October</p>
</header>
<div class="frame" data-frame>
<div class="pit" aria-hidden="true" data-pit></div>
<table class="table" aria-labelledby="ledger-title" data-table>
<thead>
<tr>
<th class="pick" scope="col">
<label class="check" data-all-check>
<input type="checkbox" data-all>
<span class="check__box" aria-hidden="true"><svg viewBox="0 0 24 24"><path class="check__tick" pathLength="1" d="M6.2 12.4l3.9 3.9 7.7-8.2"/><path class="check__dash" pathLength="1" d="M7.5 12h9"/></svg></span>
<span class="sr">Select all invoices</span>
</label>
</th>
<th class="client" scope="col">Client</th>
<th class="col-inv" scope="col">Invoice</th>
<th class="col-due" scope="col">Due</th>
<th class="num" scope="col">Amount</th>
<th class="state-cell" scope="col">Status</th>
<th class="more" scope="col"><span class="sr">Details</span></th>
</tr>
</thead>
<tbody data-rows></tbody>
</table>
<svg class="band" aria-hidden="true" data-band>
<defs>
<linearGradient id="ledger-sweep" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0" data-sweep>
<stop offset="0" class="band__stop"/>
<stop offset="0.5" class="band__stop band__stop--deep"/>
<stop offset="1" class="band__stop"/>
</linearGradient>
</defs>
<path class="band__shape" data-band-path/>
</svg>
</div>
<div class="foot" data-foot>
<p class="sum" data-sum>
<span><span class="roll" data-sum-count>6</span> invoices</span>
<span class="sum__due"><span class="roll" data-sum-due>€0.00</span> outstanding</span>
</p>
<div class="bar" data-bar>
<span class="bar__bg" aria-hidden="true" data-bar-bg></span>
<div class="bar__set" role="group" aria-label="Selected invoices" data-set="actions">
<button class="bar__icon" type="button" data-clear>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 7l10 10M17 7 7 17"/></svg>
<span class="sr">Clear selection</span>
</button>
<p class="bar__count"><span class="roll" data-picked>0</span> selected <span class="bar__sum roll" data-picked-sum>€0.00</span></p>
<span class="bar__rule" aria-hidden="true"></span>
<button class="bar__btn" type="button" data-act="paid">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.6l4.3 4.3L19 7.3"/></svg>
<span class="bar__label">Mark as paid</span>
</button>
<button class="bar__btn bar__btn--export" type="button" data-act="export">
<span class="bar__glyph" aria-hidden="true">
<svg class="bar__glyph-a" viewBox="0 0 24 24"><path d="M12 4.5v10M7.6 10.4 12 14.8l4.4-4.4M5 15.5v3.5h14v-3.5"/></svg>
<svg class="bar__glyph-b" viewBox="0 0 24 24"><path d="M5 12.6l4.3 4.3L19 7.3"/></svg>
</span>
<span class="bar__label roll" data-export-label>Export</span>
</button>
<button class="bar__btn bar__btn--danger" type="button" data-act="delete">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 7h15M9.5 4.5h5M6.6 7l.9 12.1a1.2 1.2 0 0 0 1.2 1.1h6.6a1.2 1.2 0 0 0 1.2-1.1L17.4 7M10.2 10.8v5.6M13.8 10.8v5.6"/></svg>
<span class="bar__label">Delete</span>
</button>
</div>
<div class="bar__set bar__set--toast" data-set="toast">
<p class="bar__msg" data-msg></p>
<button class="bar__btn bar__btn--undo" type="button" data-undo>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9.2 5.6 5.4 9.4l3.8 3.8M5.8 9.4h8.4a4.6 4.6 0 0 1 0 9.2h-3"/></svg>
<span class="bar__label">Undo</span>
</button>
<span class="bar__time" aria-hidden="true" data-time></span>
</div>
</div>
</div>
<p class="sr" 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.
118 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 trapdoor table” from Animorio and fit it into my siteEvery row has a piece of the shape: its top and bottom edges as shares of the row's height, its two sides in pixels in from the table's sides, all four on damped springs (x'' = −k(x − goal) − c·x', in steps of 1/240 of a second, the frame's step limited to 1/30). At every frame the pieces that touch become one outline, down the right side and back up the left, with every corner rounded, the inner ones too, as much as the sides next to it allow. The SVG path lies over the table in mix-blend-mode: multiply, so the rows stay solid and the shape tints them under the words. Two blocks about to touch, or just parted, have square corners that round off as the gap between them opens: joining and parting never jump.
A new piece starts from the little square of its box; if the row above is picked already it starts from that row's edge and stays fastened to it while it grows, so the shape stretches without coming apart, and picking every row each one starts from the edge of the one before a moment later, as if poured. Every row also has a twin piece: between two blocks the row picked reaches out to both of them, and let go in the middle of a block it splits into its two halves, which go back each to its own neighbour. Going back into its box a piece first slides in sideways and then shrinks, so it never becomes a thin line over the name.
The bar lives in the table's footer: the summary goes and a dark square rolls out to the right as wide as what it says, with a little overshoot; its parts are cut by the edge going by (clip-path) and come in one after the other as it passes them. The number of rows picked rolls over, the sum counts, and Mark as paid dims when none of the rows picked is waiting to be paid. The actions that can be undone turn the bar into the message with Undo and a line running out in UNDO_FOR seconds; the focus always moves to the right button before the old one goes.
Delete takes every run of rows next to each other and makes it a door: a copy of the rows, with the same column widths, under the table, in the same place. The real rows leave the DOM at once and the ones below are taken back to where they were with a transform (FLIP). The door turns round its top edge with perspective and rotateX(−φ), pulled by gravity, φ'' = g·cos φ, with g worked out so that it is edge-on after DROP seconds, and it darkens with the sine of the angle. When the door is at 35 degrees the rows below set off all together on the same spring, each as far as it has to go: so every hole closes in proportion and the table never tears where there was no hole. The dark between the rows is drawn again at every frame in SVG, a rectangle for every gap, darker under the edge above, and nothing shows below the footer.
The rows go back to their places in the DOM but hidden; the ones below start from where they were and move down to make room, opening the dark holes, and in every hole its door swings back up from edge-on with a steady lift, slaps flat and bounces once (restitution 0.26). When it stops, the real rows take its place with their shape already whole, picked as they were, and the bar goes back to the actions. Undoing Mark as paid rolls the states back and brings the selection back.
It is a table with its heads, the boxes are real inputs with a hidden label (Select INV-2042, Mirabel Studio), the box in the head has the indeterminate state, the details are a row with aria-controls and aria-expanded, and an aria-live region says how many invoices are picked, deleted or marked. Esc clears the selection. Dragging over the boxes holds the pointer only once it reaches another row, and the click that ends the stroke is thrown away. On a phone Invoice and Due move under the name, the type narrows with the width axis (90%) and the details' cell spans exactly the columns that are left. A click from a script (the library's preview) picks, deletes, undoes, picks all, marks as paid, opens a row and exports, one thing per click.
| Name | File | Default | What it changes |
|---|---|---|---|
INVOICES | script.js | [['Halden Moss', 'INV-2041', 'Sep 30', 2400, 'paid'], …] | The invoices: client, number, due date, amount in euros and state (paid, overdue, sent or draft). |
DETAILS | script.js | [['Brand guidelines, second round', 'Ines Halden', 'Art direction, 3 days', 1500, 'Type licences', 'Sent Sep 16|Opened Sep 17|Paid Sep 29'], …] | Each invoice's details when its row opens: project, person, a first line with its amount, a second line (the rest of the amount) and what has happened, split by |. |
SPRING | script.js | [360, 0.56] | The springs of the selection's shape: how stiff, and how damped, from 0 (wobbles a lot) to 1 (does not wobble). |
RADIUS | script.js | 12 | The radius of the shape's corners, in pixels. |
DROP | script.js | 0.38 | The seconds deleted rows take to fall through the trapdoor. |
CLOSE | script.js | [320, 0.78] | The springs of the rows closing the holes: how stiff, and how damped. |
CHAIN | script.js | 0.05 | The seconds between one trapdoor and the next, when rows go from different places. |
UNDO_FOR | script.js | 6 | The seconds Undo stays in the bar. |
For a studio's invoices, a shop's orders, the members of a team, a newsletter's subscribers, the files of an archive: any back office where rows are picked to do something to all of them at once.
At 390 pixels the Invoice and Due columns go and the number and due date move under the client's name; the type narrows to 90% with the width axis, the rows are 58 pixels tall and the boxes are 44 pixel targets. The bar keeps its icons (the words stay for screen readers). Dragging a finger down the boxes picks many rows; anywhere else the finger scrolls the page.
Nothing grows or falls: each picked row's shape fades in and out already in place, deleted rows fade away and the others are at once in their places, the ones brought back fade in, the bar and the summary fade into each other and the details open already open.
GSAP 3.15.0, Archivo (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 23 KB; the libraries: GSAP 28 KB.
Tested on 6 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.