Skip to content
animorio
IT
Back to the library

Captions that read aloud

Text, ScrollGSAP, CanvasAdvancedCompressed weight: Demo 16 KBGSAP + ScrollTrigger 45 KBFreeDownload the zip

Scroll inside the preview

A comic in four engraved panels. When a caption comes on screen its box unrolls from the left edge and its lines come in one after another; then a narrator reads it, one red word at a time, at the pace of a real voice, and waits if you scroll in a hurry.

The code

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>Captions that read aloud</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/im-fell-english@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/im-fell-english@5.3.0/400-italic.css">
  <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>
  <header class="bar">
    <a class="brand" href="#">Tallow &amp; Ink</a>
    <nav class="bar__nav" aria-label="Sections">
      <a href="#">Books</a>
      <a href="#">Pre-order</a>
    </nav>
  </header>

  <main>
    <section class="intro">
      <p class="kicker">A graphic novel in six chapters</p>
      <h1 class="title" data-title>The Night Ferry</h1>
      <p class="lede">Scroll down slowly. The narrator reads each caption as it arrives, and waits whenever you hurry.</p>
    </section>

    <!-- the story: four panels drawn by the script as engravings, each with its caption; a caption unrolls from its
         left edge when it comes on screen, then the narrator reads it, one word at a time -->
    <section class="strip" aria-label="Chapter one">
      <figure class="panel panel--wide" data-panel="harbour">
        <canvas class="panel__art" data-art aria-hidden="true"></canvas>
        <figcaption class="caption caption--top" data-caption>Every night at eleven the last ferry leaves Corran harbour with its lamps lit and nobody aboard.</figcaption>
      </figure>

      <figure class="panel panel--right" data-panel="shore">
        <canvas class="panel__art" data-art aria-hidden="true"></canvas>
        <figcaption class="caption caption--bottom" data-caption>The ferryman counts the lights along the shore, one by one, until the fog has taken all of them.</figcaption>
      </figure>

      <figure class="panel panel--left" data-panel="bell">
        <canvas class="panel__art" data-art aria-hidden="true"></canvas>
        <figcaption class="caption caption--top caption--end" data-caption>Halfway across, a bell rings out where no buoy has ever been.</figcaption>
      </figure>

      <figure class="panel panel--wide" data-panel="answer">
        <canvas class="panel__art" data-art aria-hidden="true"></canvas>
        <figcaption class="caption caption--bottom" data-caption>He slows the engine and lifts his lantern. Far below, the water answers with a light of its own.</figcaption>
      </figure>
    </section>

    <section class="end">
      <p class="kicker">Chapter one is free to read</p>
      <h2 class="end__title">Out on 14&nbsp;November</h2>
      <div class="end__actions">
        <a class="button" href="#">Read chapter one</a>
        <a class="button button--ghost" href="#">Pre-order the book</a>
      </div>
    </section>
  </main>

  <!-- the narrator: what it is doing, and the switch for a real voice where the browser has one -->
  <aside class="narrator" data-narrator aria-live="off">
    <span class="narrator__bars" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
    <span class="narrator__state" data-state>Narrator: quiet</span>
    <button class="narrator__voice" type="button" data-voice aria-pressed="false" hidden>
      <svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true"><path d="M3 7.5h3l4-3.5v12l-4-3.5H3z" fill="currentColor"/><path d="M13 7a4.2 4.2 0 0 1 0 6M15.4 4.8a7.4 7.4 0 0 1 0 10.4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
      <span data-voice-label>Read aloud</span>
    </button>
  </aside>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.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.

41 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 “Captions that read aloud” from Animorio and fit it into my site

What it does

The teaser of an invented comic, The Night Ferry, by an invented publisher, Tallow & Ink. The title comes in letter by letter from both sides towards the middle, the middle ones first. Below are four panels: the ferry leaving the harbour under the moon, the town’s lights eaten by the fog, a bell buoy out at sea, a lantern raised over black water and a red light rising from the deep. The panels are engravings drawn by the script, with lines that swell in the shadows and break into dots in the light; they are redrawn eight times a second with the lines moved slightly, so the drawing looks handmade and alive. Each panel has its caption: when it gets a little below the middle of the screen the box unrolls from its left edge and the lines come in from that edge, one after another. Then the narrator reads it: each word turns red a moment before it is read and goes back to black just after, with the timings of a recorded voice, pausing at commas and full stops. It reads one caption at a time, in the order they arrived, and only starts once the scroll has calmed down; the box at the bottom says whether it is reading, waiting or quiet. If a caption leaves while it is being read, it fades and the next one starts; when it comes back it unrolls and is read again. Where the browser can speak, «Read aloud» gives the narrator a voice, and the red words follow it.

How it works

The engraved panels

Each scene is painted in greys on three hidden canvases: the tone, from black to white, the angle of the lines and how much accent colour. Then every pixel becomes ink, paper or red by comparing the tone with a field of wavy lines SPACING pixels apart, as in the site’s shader: below a first threshold it is ink, above it the colour, above a second threshold paper, and the lines push both thresholds about, so the edges of the shadows are hatched. In the dark half of each line is ink, in the light none; in between the lines break into dots.

The boiling drawing

Each panel is engraved three times, with a slightly different wobble in the lines and a frame crooked another way, and the three drawings take turns BOIL times a second. They are prepared when the panel is one screen away, one drawing at a time, so the page never stalls.

The box unrolls

When the caption’s top edge passes ENTER per cent of the screen, the box starts at FROM of its width and unrolls in UNROLL seconds with the original’s cubic-bezier(0.45, 0.30, 0.16, 1), while the whole caption comes in from SHIFT pixels to the left. The box widens with scaleX, the text does not: a clip-path following the box’s edge cuts it. Each line comes in from the left edge in SLIDE seconds (power3.out), STAGGER seconds after the one before.

The voice’s timings

Each word lasts VOICE.perLetter seconds for each letter plus VOICE.base, never less than VOICE.shortest; between two words comes VOICE.gap, after a comma VOICE.comma, after a full stop VOICE.stop. These are the averages measured on the words of the site’s narrating voice. A word is red from LEAD seconds before it starts to LEAD after it ends, with a smoothstep; PACE speeds everything up or slows it down.

The narrator’s queue

Captions that arrive join a queue. The narrator takes the first one only when it is not reading already and the page has stayed under CALM pixels a frame for WAIT seconds. A caption that leaves the screen leaves the queue, or fades out in 0.4 seconds if it was the one being read; with REPLAY it rolls up and is read again when it comes back.

A real voice

With «Read aloud» the narrator uses the browser’s speech synthesis. At every word the voice reaches (the boundary event) the words’ clock is put back on that word, so the red follows the voice even when it goes at another pace; where the event does not come, the red follows the computed timings.

Values to change

NameFileDefaultWhat it changes
UNROLLscript.js0.8The seconds the box takes to unroll from its left edge.
FROMscript.js0.25How much of the box is already there when it starts to unroll (0 to 1).
SHIFTscript.js40How many pixels to the left the whole caption comes in from.
SLIDEscript.js2The seconds each line takes to come in from the left edge.
STAGGERscript.js0.2The seconds between one line and the next (three quarters of it past four lines).
LEADscript.js0.4The seconds a word takes to turn red before it is read, and to go back after.
PACEscript.js1The narrator’s speed: 1 is the pace of the recorded voice.
CALMscript.js15The scroll speed, in pixels a frame, under which the narrator may start.
WAITscript.js0.1The seconds of calm scrolling before the narrator reads the next caption.
ENTERscript.js62Where a caption comes in: its height on the screen, from the top, as a percentage.
REPLAYscript.jstrueA caption that has left the screen rolls up and is read again when it comes back.
BOILscript.js8How many times a second the engravings change drawing.
SPACINGscript.js4.2The distance between the lines of the engravings, in pixels.
VOICEscript.js{ base: -0.036, perLetter: 0.076, shortest: 0.12, gap: 0.06, comma: 0.68, stop: 0.92, first: 0.23 }The voice’s timings, in seconds: how long a word lasts for each letter (perLetter, plus base, never under shortest), the pause between words, after commas and after full stops, and the wait before the first word.

Try them in the Playground

When to use it

For a story told in panels: a comic, a brand’s story, a guided tour, the captions of a silent video. The waiting narrator works best with short captions of two or three lines; the engravings can be replaced with real pictures without touching the captions.

On phones

The panels turn upright (4:5) and the captions narrower, with the same narrator and voice. Scrolling with a finger counts like the wheel: a fast flick makes the narrator wait until the page slows down.

With reduced motion

No letters flying in, no boxes unrolling and no boiling drawings: the captions are already there and the panels stay still. The narrator still reads, because the red passing over the words changes a colour and moves nothing.

Pitfalls

  • If the text widened with the box the letters would be squashed: the box unrolls with scaleX on a layer of its own, and the text is cut by a clip-path that follows its edge every frame.
  • The lines are found once the typeface has arrived, because the fallback breaks them in other places. The words stay the same elements and only the lines holding them change, so the red of the reading is never lost.
  • Calm does not mean still: with a threshold of zero the narrator would never start while you scroll slowly to read. What counts is the speed in pixels a frame, brought back to 60 frames a second.
  • In Safari speech synthesis only starts after a gesture: the first «Read aloud» is a click, and from then on readings start from the scroll too. With some voices Chrome sends no boundary events: that is why the red always has its own computed timings as well.
  • The three engravings of a panel are prepared one at a time, one job per setTimeout round, and only when the panel is near: all together they would stall the page for a few tenths of a second.

Libraries

GSAP 3.15.0, IM Fell English 5.3.0, Archivo (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 16 KB; the libraries: GSAP + ScrollTrigger 45 KB.

Testing

Tested on 5 October 2026

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

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 animations