Skip to content
animorio
IT
All components

The sentence that inks in

Loading, Text, TransitionsGSAPAdvancedCompressed weight: Demo 13 KBGSAP 28 KBFreeDownload the zip

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.

The code

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>

Playground

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

Or ask Claude Code for it

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 site

What it does

On a black background, alone in the middle: a large sentence over three lines, Exporting 248 photos to your archive, in the thinnest and widest cut of Archivo, in a warm grey; under it 0% in tabular figures on the left and 0 of 248 on the right; a thin line; at the foot Originals · 2.6 GB and the pill button Export, with a thin edge. Coming in, the letters rise by a few pixels one after the other and the line draws itself from the left. Press Export and the letters take ink one after the other, from the E on: they turn solid, light on the black, and heavy, but stay exactly where they are, because each one narrows on the width axis just as much as it needs to stay as wide as it was when thin. The change is not sharp: the four letters at the front of the wave are halfway, and each letter, as its ink lands, sinks by a pixel and comes back, and the two next to it feel the blow. Below, the percentage turns like an odometer, the units passing through every figure, the line fills and its head is a short vermilion stroke; on the right the count of photos, at the foot the gigabytes in. Pause holds the wave where it is, halfway through a word, the head dims a little and the line says Paused at 1.20 GB; Resume carries on. One time in two the connection is lost: a vermilion stroke stays where it stopped, the line says Connection lost · 124 photos kept in vermilion, and the ink comes off the letters going backwards down to the photos kept while the drums turn back; the letters getting lighter rise by a hair. Retry carries on from there. At 100% the letters that are not needed, Exporting, to and your, lose their weight and fade as they rise; photos slides right to make room, then 248 runs along the first line and drops into place: the words move whole, like slugs of lead in a printer’s hands, and as they fly they lose a little ink that they take back as they land. Last, the d is printed: 248 photos archived. Export again plays it all backwards, the thin sentence setting itself again, and starts over.

How it works

Every letter in its cell

The 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.

Weight that moves nothing

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 weight wave and the press

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.

The drums of the percentage

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 sentence sets itself from its own letters

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.

The preview’s round

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.

Values to change

NameFileDefaultWhat it changes
PHRASEscript.js'Exporting 248 photos to your archive'The sentence while the photos are exported: it is the progress bar.
DONEscript.js'248 photos archived'The sentence at the end, set again from the letters it shares with the first.
PHOTOSscript.js248How many photos are exported (the count under the sentence).
SIZE_GBscript.js2.6How big the export is, in gigabytes.
SECONDSscript.js6How many seconds an export takes in the demo (a real one follows its bytes).
WEIGHTscript.js[100, 900]The weight (wght) of a letter not printed yet, and of a printed one.
WIDTHscript.js125The width (wdth) of the thin letters: the printed ones narrow just enough to keep their place.
SOFTscript.js4Over how many letters the edge of the weight wave is softened.
PRESSscript.js1.2How many pixels a letter sinks when its ink lands.
FAIL_EVERYscript.js2One export in how many loses the connection (0 never).
FAILscript.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.
PREVIEWscript.js[1.7, 0.46]In the preview’s round: how many times faster, and where it pauses.
LABELSscript.js['Export', 'Pause', 'Resume', 'Retry', 'Export again']The words of the button, state by state.
--inkstyle.css#f4f1eaThe printed letters, the figures and the line.
--lightstyle.css#a8a49cThe letters not printed yet.
--accentstyle.css#ff5a2aThe head of the line, and what went wrong.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • The first measuring gave every letter the same width, the page’s: the probe was a block and stretched to its container. Now every letter in the probe is an inline-block and the probe sits out of the flow, absolutely positioned.
  • Bisecting letter by letter asked for a layout for every letter, every weight and every step: thousands. Done for all the letters together (all the trial wdths written first, then all the widths read) 176 are enough.
  • The first pairing of the shared letters flew them one by one with a stagger: the letters of photos, moving right, caught up with each other and overlapped. Now they fly as whole words, and within a word the letter on the leading edge goes first.
  • On a diagonal flight 248 passed over photos, already in place. A word that changes line now travels along the higher of the two lines, and only goes once whoever stands in its place has left (a topological sort on the boxes).
  • The letters leaving the sentence were no longer in the list the loop draws, so the d stayed heavy while the rest got lighter: now the loop draws them too until they have faded.
  • A tap while the sentence was coming in was lost, because the component was still in its entrance state: now the sentence can be used as soon as it is laid out, even halfway through the entrance, and a tap that comes before the typeface has loaded waits and goes right after.

Libraries

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.

Testing

Tested on 16 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

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.

All components