
The menu that opens from its button
Menus, Transitions, Text
Free
In the minimal style: 248 photos being exported to an archive, and the progress bar is the sentence itself, Exporting 248 photos to your archive. As the work goes on the letters go from a hairline to full weight one after the other, as if the sentence were being printed, on a weight wave softened over four letters, without the sentence moving by a pixel. Under it the percentage on turning drums and a thin line. Pause, resume, a lost connection that lifts the ink off from where it stopped, and at the end the sentence sets itself again as 248 photos archived, from its own letters.
A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The sentence that prints itself</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/archivo@5.3.0/wdth.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- a photo app exporting to an archive, and the progress bar is the sentence itself: as the work goes on its
letters go from a hairline to full weight one after the other, as if it were being printed, on a weight wave
a few letters soft. Under it the percentage on rolling figures and a thin line. Pause, resume, a lost
connection that lifts the ink back off, and at the end the sentence sets itself again from its own letters -->
<section class="ex" aria-label="Photo export" data-export>
<p class="ex__phrase" aria-hidden="true" data-phrase></p>
<p class="sr" data-words>Exporting 248 photos to your archive</p>
<div class="ex__row">
<span class="ex__pct" aria-hidden="true" data-pct></span>
<span class="ex__count" aria-hidden="true" data-count>0 of 248</span>
</div>
<div class="ex__line" role="progressbar" aria-label="Export" aria-valuemin="0" aria-valuemax="100"
aria-valuenow="0" aria-valuetext="Not started" data-line>
<span class="ex__track" data-track></span>
<span class="ex__fill" data-fill></span>
<span class="ex__mark" data-mark></span>
<span class="ex__head" data-head></span>
</div>
<div class="ex__foot">
<span class="ex__swap" data-status>Originals · 2.6 GB</span>
<button class="ex__btn" type="button" data-btn data-autoclic>
<span class="ex__btnfill" aria-hidden="true"></span>
<span class="ex__label" data-label>Export</span>
</button>
</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.
31 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 sentence that inks in” from Animorio and fit it into my siteThe sentence is split into words (a wrapping flex) and every letter is a cell as wide as the letter in the thin cut, wght 100 and wdth 125, measured in em: the sentence breaks its lines once, the same way for the whole load. The gap between the words is a space of the same cut, measured as the difference between “n n” and “nn”. The cell is the element GSAP moves; the letter inside is the one the script gives its weight, width, tone and sinking, so the two transforms never cancel each other.
A heavier letter is wider. At the start the script measures, for every letter of both sentences and for seventeen weights between 100 and 900, the wdth at which the letter is exactly as wide as its cell: an eleven-step bisection, done for all the letters together (each gets its own trial wdth, then all the widths are read), so every step costs one layout, not one per letter. While loading, every letter’s wdth is read from this table, interpolated between two weights: the heavy O narrows to about 100, the i cannot quite make it and runs over by half a pixel, which its side bearings hide.
The work done, from 0 to 1, becomes a front running along the sentence: for letter i the goal is smoothstep((p·(n + SOFT) − i) / SOFT), so SOFT letters are halfway. Every letter follows its goal on a spring just under critical damping (0.74), integrated every frame with a step of at most 1/30 of a second. When the ink passes 90% the letter gets a push downwards on a softer spring (0.42), its neighbours a third and a tenth: it sinks by PRESS pixels and comes back with a settle. When the connection is lost and the weight drops under 40%, the push is small and upwards.
Every figure is a drum with 0 to 9 and 0 again in a window one figure tall, faded at the top and bottom by a mask. The units drum follows the whole count of the percentage, the tens drum that count over ten, the hundreds over a hundred, on a spring: the offset is the count modulo ten, so going from 9 to 10 turns forward without a jump. A jump of more than six figures (back to nought) takes the short way round. A drum that is not needed closes up in width with a small bounce.
The letters the two sentences share are found the way a diff does it (Ratcliff and Obershelp): the longest common run, 248photos, then the same search on its left and right, which finds archive. The letters that stay are grouped into words and fly with FLIP: measured first, the layout redone with the new sentence, measured again, and they start from the difference. A word only goes once the words standing where it is going have left (a small topological sort on their boxes), and a word that changes line travels along the higher of the two: going down it slides first and then drops, going up it rises first and then slides. The letters that leave stay where they were, taken out of the flow with absolute positions, and fade as they rise; the new ones are printed in place.
A click from a script on the button (detail 0, not the keyboard) plays the whole round, faster by PREVIEW: a pause at 46%, resume, the lost connection, Retry, the final sentence and back to rest, with a pretend press on the button at every step; the waits between the steps are GSAP delayedCalls and other script clicks during the round are ignored. A real click stops the round and takes over. When nothing moves the loop leaves GSAP’s ticker and draws nothing.
| Name | File | Default | What it changes |
|---|---|---|---|
PHRASE | script.js | 'Exporting 248 photos to your archive' | The sentence while the photos are exported: it is the progress bar. |
DONE | script.js | '248 photos archived' | The sentence at the end, set again from the letters it shares with the first. |
PHOTOS | script.js | 248 | How many photos are exported (the count under the sentence). |
SIZE_GB | script.js | 2.6 | How big the export is, in gigabytes. |
SECONDS | script.js | 6 | How many seconds an export takes in the demo (a real one follows its bytes). |
WEIGHT | script.js | [100, 900] | The weight (wght) of a letter not printed yet, and of a printed one. |
WIDTH | script.js | 125 | The width (wdth) of the thin letters: the printed ones narrow just enough to keep their place. |
SOFT | script.js | 4 | Over how many letters the edge of the weight wave is softened. |
PRESS | script.js | 1.2 | How many pixels a letter sinks when its ink lands. |
FAIL_EVERY | script.js | 2 | One export in how many loses the connection (0 never). |
FAIL | script.js | [0.72, 0.5] | Where the connection is lost and how far back the ink is lifted (the photos kept), from 0 to 1. |
PREVIEW | script.js | [1.7, 0.46] | In the preview’s round: how many times faster, and where it pauses. |
LABELS | script.js | ['Export', 'Pause', 'Resume', 'Retry', 'Export again'] | The words of the button, state by state. |
--ink | style.css | #f4f1ea | The printed letters, the figures and the line. |
--light | style.css | #a8a49c | The letters not printed yet. |
--accent | style.css | #ff5a2a | The head of the line, and what went wrong. |
For an export, an upload or a backup in a photo or documents app, for publishing an article, for sending an order or a form, for any wait where the sentence that says what is happening can be the progress itself.
The component takes the whole width less the margins (up to 400 pixels) and the sentence gets smaller with the phone, its cells in em, so it breaks its lines as on the computer and never moves; the animations are the same. The button answers a touch as it does the mouse: it fills and its words widen while the finger presses. If the window changes size while the sentence is setting itself again, that jumps to its end.
Nothing moves: the weight of the letters still changes, because it is the progress, but with no spring and no sinking; the figures change without turning, the lines change with a fade and the final sentence fades in instead of setting itself with flying letters.
GSAP 3.15.0, Archivo (font variabile, assi wght e wdth) 5.3.0.
Compressed with gzip, the three demo files weigh 13 KB; the libraries: GSAP 28 KB.
Tested on 16 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.