
The trapdoor table
Tables, Physics, SVG, Transitions, 3D
NewFree
The contact form of a stationer written on a sheet of laid letter paper, with its grain, in blue black ink. Mistakes are corrected on the sheet in pencil, as a proofreader would: a loop round an empty line, a wavy line and a caret where the email’s ending is missing, a bracket by a letter too short; once the field is fixed the pencil is rubbed out and leaves a smudge. Sent, the sheet folds in three in 3D with real creases, light and shade on every face, slides into a lined envelope, the flap closes, the sender is written on it, the envelope turns over, takes a stamp and a postmark and flies away; then the clean sheet comes back with Sent. We’ll write back.
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 form that folds into an envelope</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bodoni-moda@5.3.0/opsz.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bodoni-moda@5.3.0/opsz-italic.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/caveat@5.3.0/index.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/shantell-sans@5.3.0/infm.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the contact form of a stationer, written on a sheet of laid letter paper: a mistake is corrected in pencil
on the sheet; sent, the sheet folds in three in 3D, slides into a lined envelope, the flap closes, the
envelope turns over, takes a stamp and a postmark and flies away. Sending is pretended: see post() in the
script -->
<div class="post" data-post data-autoclic>
<div class="desk" data-desk>
<span class="ground" aria-hidden="true" data-ground></span>
<form class="sheet" action="#" novalidate aria-labelledby="post-title" data-sheet>
<div class="paper" data-paper>
<header class="head">
<p class="head__from">
<svg class="head__mark" viewBox="0 0 28 28" aria-hidden="true"><path d="M14 2.5 25.5 14 14 25.5 2.5 14z"/><path d="M11 19.5V8.6h4.1c2.3 0 3.7 1.3 3.7 3.2s-1.4 3.2-3.7 3.2H11"/></svg>
<span>Quillmere Paper Co.</span>
</p>
<h2 class="head__title" id="post-title" tabindex="-1">
<span class="head__line" data-title-rest>Write to us.</span>
<span class="head__line head__line--sent" aria-hidden="true" data-title-sent>Sent. We’ll write back.</span>
</h2>
<p class="head__sub">
<span class="head__line" data-sub-rest>Wedding suites, letterheads, a box of samples: every letter is answered by hand.</span>
<span class="head__line head__line--sent" aria-hidden="true" data-sub-sent>Your letter is on its way to the letter desk. Our answer comes within two days.</span>
</p>
</header>
<div class="field" data-field="name">
<label class="field__label" for="post-name">Your name</label>
<div class="field__rule" data-rule>
<input class="ink" id="post-name" name="name" type="text" autocomplete="name" autocapitalize="words" spellcheck="false" enterkeyhint="next" aria-describedby="post-name-note" aria-invalid="false" data-input>
</div>
<p class="note" id="post-name-note" data-note></p>
</div>
<div class="field" data-field="email">
<label class="field__label" for="post-email">Where we write back</label>
<div class="field__rule" data-rule>
<input class="ink" id="post-email" name="email" type="text" inputmode="email" autocomplete="email" autocapitalize="off" autocorrect="off" spellcheck="false" enterkeyhint="next" placeholder="you@example.com" aria-describedby="post-email-note" aria-invalid="false" data-input>
</div>
<p class="note" id="post-email-note" data-note></p>
</div>
<div class="field field--letter" data-field="letter">
<label class="field__label" for="post-letter">Your letter</label>
<div class="field__rule field__rule--lines" data-rule>
<textarea class="ink ink--letter" id="post-letter" name="letter" rows="3" spellcheck="true" aria-describedby="post-letter-note" aria-invalid="false" data-input></textarea>
</div>
<p class="note" id="post-letter-note" data-note></p>
</div>
<div class="foot">
<p class="foot__sign">Yours, <span class="foot__name" data-sign></span></p>
<button class="send" type="submit" data-send>
<span class="send__bg" aria-hidden="true"></span>
<span class="send__label">Send the letter</span>
<svg class="send__icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3.5 6.5h17v11h-17z"/><path d="m3.8 6.8 8.2 6.4 8.2-6.4"/></svg>
</button>
</div>
<svg class="pencil" aria-hidden="true" data-pencil>
<defs>
<filter id="post-graphite" x="-10%" y="-30%" width="120%" height="160%">
<feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" seed="7" result="grain"/>
<feColorMatrix in="grain" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.5 1.45" result="tooth"/>
<feComposite in="SourceGraphic" in2="tooth" operator="in"/>
</filter>
<filter id="post-smudge" x="-20%" y="-60%" width="140%" height="220%">
<feGaussianBlur stdDeviation="2.4"/>
</filter>
</defs>
</svg>
</div>
</form>
<!-- what the sheet becomes when it is sent: drawn for the eye only, the form itself stays above -->
<div class="env" aria-hidden="true" data-env>
<div class="env__turn" data-turn>
<div class="env__back" data-back>
<span class="env__wall" data-wall></span>
<div class="flap" data-flap>
<span class="flap__in" data-flap-in></span>
<span class="flap__out" data-flap-out>
<span class="flap__from" data-from><span class="flap__who" data-from-name></span><span class="flap__mail" data-from-mail></span></span>
</span>
</div>
<div class="fold" data-fold></div>
<svg class="pocket" data-pocket></svg>
<span class="env__shade" data-env-shade></span>
</div>
<div class="env__front" data-front>
<span class="env__border"></span>
<span class="airmail">By air mail</span>
<span class="env__to">
<span class="env__to-desk">The Letter Desk</span>
<span>Quillmere Paper Co.</span>
<span>4 Ropewalk Yard</span>
<span>Quillmere</span>
</span>
<span class="stamp" data-stamp>
<svg viewBox="0 0 60 72" data-stamp-art></svg>
</span>
<svg class="cancel" data-cancel></svg>
</div>
</div>
</div>
</div>
<p class="sr" role="status" 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.
48 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 form that folds into an envelope” from Animorio and fit it into my siteTwo inputs and a textarea, real, with their labels (autocomplete name and email, inputmode email, text at 27 pixels, over the 16 Safari on phones needs not to zoom), aria-invalid and aria-describedby pointing at the pencil’s word under each field, which is real text and is read out. Enter in the name moves to the email, in the email to the letter, Ctrl or Cmd with Enter in the letter sends it. On a wrong send the focus goes to the first field to fix and an aria-live region says what to check; once sent it reads Sent. We’ll write back. and the focus goes to the title. Sending is pretended in one function, post(), the one to replace with the real call.
The marks are drawn in SVG over the sheet, measuring where the fields sit with offsetTop and offsetLeft (which ignore transforms) and where the words end with measureText on a canvas, in the field’s own typeface. The loop is a little more than a turn, its radius wavering with two sines and growing a little, so its start and end do not meet; the wavy line, the caret, the bracket and the tick are points with a random but repeatable tremor (a seeded generator), joined by Catmull-Rom curves. Every stroke has pathLength 1 and draws itself with stroke-dashoffset; the graphite is an SVG filter where feTurbulence punches holes in the line. Rubbed out, the strokes move to a group of their own and fade while a wide blurred copy makes the smudge; on resize the marks are drawn again where the fields have gone.
On sending, the sheet is copied six times as it is (with what is written on it, without ids, names or aria attributes), and every copy sits inside a face with overflow hidden, shifted by a third: three panels, each with a front and a back, the back turned with rotateX(180) and backface-visibility hidden. The middle panel holds the other two as children in preserve-3d: the lower one turns on its top edge, the upper one on its bottom edge. Every hinge sits half a paper’s thickness above the sheet (translateZ, rotateX, translateZ back), the second a little higher than the first: so, folded, the layers stack in the right order without flicker. The back shows the writing through, upside down and blurred, at five per cent.
On every frame the normal of every face is worked out: all the rotations are about the same axis, so the angles add up (the tilt of the sheet plus that of the fold) and the normal is (0, −sin a, cos a). Its dot product with the lamp’s direction, less that of the flat sheet, says how much the face lightens or darkens, and a layer over the face takes that warm colour. A fresh fold springs open by RELAX degrees and the second one presses it flat when it lands; while the thirds close, a shadow grows on the middle face, the edge of the fold catches a thread of light and at the end a gloss runs along the crease like a bone folder. Underneath, a shadow on the desk follows the outline of the folded sheet and widens and fades when a third is lifted.
The back of the envelope is a single plane with its layers in z-index order: the lining, the flap, the letter, the side and bottom flaps drawn in SVG (redrawn in pixels when the size changes) and the flap’s shadow. The letter sits inside the envelope from the start, so it really passes between the lining and the flaps; its position is written less that of the envelope. The flap is a flat plane turning with rotateX: past 90 degrees the lining shows, under 90 it goes over the flaps by changing its z-index, and it darkens or lightens with the same light. The front is the other face of a container in preserve-3d turning with rotateY; the stamp has its perforations made with a mask of circles, the postmark a roller that is a mask rectangle widening and an uneven ink made with feTurbulence and feDisplacementMap. The flight is a quadratic Bézier curve; everything is one GSAP timeline calling render() on every frame. A click from a script (detail 0, the preview) makes the whole journey without taking the focus: it writes, gets the email wrong, the pencil corrects it, the hand adds the ending, sends, and at the end the title goes back to rest.
| Name | File | Default | What it changes |
|---|---|---|---|
FOLD | script.js | 0.6 | How many seconds each third of the sheet takes to fold over. |
RELAX | script.js | 7 | How many degrees a fresh fold springs open before the next third presses it flat: 0 is paper that stays down. |
TILT | script.js | 12 | How many degrees the sheet leans back while it folds, to show its thickness. |
FLAP | script.js | 0.55 | How many seconds the envelope’s flap takes to close. |
HOLD | script.js | 0.9 | How many seconds the stamped envelope stays on the desk before it flies away. |
FLIGHT | script.js | 0.85 | How many seconds the envelope’s flight lasts. |
SETTLE | script.js | 0.7 | The seconds of quiet after the last key before a field is read over. |
SHORTEST | script.js | 12 | The fewest characters the letter may have. |
TOWN | script.js | QUILLMERE | The town written round the postmark. |
STAMP_VALUE | script.js | 1.85 | The value printed on the stamp. |
On the contact page of anyone working with paper and words: a stationer, a printer, a design studio, a wedding photographer, a small hotel that answers by hand. The envelope leaving is the receipt the writer takes away: they know the message has gone, and to whom. The pencil correction suits any form: it does not shout while you type, waits for the field to be left and goes as soon as it is right. For another brand change the colours, the address, the stamp and the postmark.
The sheet takes the width of the screen (358 pixels on a 390 phone) and the envelope 94 per cent of the sheet, so nothing sticks out at the sides; the flight goes upwards and the envelope is gone before the edge. The fields are 40 pixels tall and the letter 108, the button 50, all over the 44 of a finger; the text is at 27 pixels, so Safari does not zoom the page on a tap; the email opens the keyboard with the @ and the Enter key says Next. The button, which on a computer swells under the pointer and changes the cut of its letters, squashes under a tap. The same folds, the same envelope and the same flight, at the phone’s size.
No folds, no flight and no strokes running: the pencil marks and the words under the fields fade in and out. On sending the sheet fades into the envelope already closed and turned over, then the stamp and the postmark fade in; after the same moment of waiting the envelope fades away and the clean sheet fades back with Sent. We’ll write back. The title changes by crossing over, without moving.
GSAP 3.15.0, Bodoni Moda (font variabile) 5.3.0, Caveat (font variabile) 5.3.0, Shantell Sans (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 22 KB; the libraries: GSAP 28 KB.
Tested on 17 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.