Skip to content
animorio
IT
Back to the library

The proofreader's marks transition

Transitions, TextGSAP, CSS onlyMediumCompressed weight: Demo 7.1 KBGSAP 28 KBFreeDownload the zip

A minimal page change made like correcting a proof: on a click a red line, wavering like a pen, strikes the title out line by line and the «del» mark appears in the margin; the letters leave one at a time from the end, narrowing. Then a caret «^» pops up on the baseline, «ins» goes in the margin, and the new title comes out of the caret letter by letter, narrow then wide, a pixel past its place and back.

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>The proofreader's marks transition</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/roboto-flex@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage" data-stage>
    <header class="bar">
      <span class="brand">Galley</span>
      <nav class="nav" aria-label="Pages">
        <a href="#draft" data-go="a" data-nav="a" aria-current="page">Draft</a>
        <a href="#proof" data-go="b" data-nav="b">Proof</a>
      </nav>
    </header>

    <!-- the margin where the proofreader writes the marks -->
    <aside class="margin" data-margin aria-hidden="true">
      <div class="mark mark--del" data-mark="del">
        <svg viewBox="0 0 40 56"><path data-draw d="M6 8 C 14 8 26 6 30 14 C 34 22 22 28 18 22 C 14 16 24 10 30 18 C 34 26 30 42 22 50 C 18 54 12 52 14 48"/></svg>
        <span class="mark__word">del</span>
      </div>
      <div class="mark mark--ins" data-mark="ins">
        <svg viewBox="0 0 40 56"><path data-draw d="M8 40 L20 16 L32 40"/><path data-draw d="M20 16 L20 6"/></svg>
        <span class="mark__word">ins</span>
      </div>
    </aside>

    <section class="page page--a is-on" data-page="a" aria-label="Draft">
      <div class="col">
        <p class="kicker" data-soft>The draft</p>
        <h1 class="title"><span class="line" data-line>Rough words,</span><span class="line" data-line>first thoughts.</span></h1>
        <p class="text" data-soft>Everything we write starts here, loose and too long. Then the red pen comes.</p>
        <a class="go" href="#proof" data-go="b" data-autoclic data-soft>
          <span class="go__text">Send it to proof</span>
          <span class="go__arrow" aria-hidden="true"><span class="go__shaft"></span><svg class="go__head" viewBox="0 0 8 12"><path d="M1.5 1.5 6.5 6l-5 4.5"/></svg></span>
        </a>
      </div>
    </section>

    <section class="page page--b" data-page="b" aria-label="Proof" aria-hidden="true" inert>
      <div class="col">
        <p class="kicker" data-soft>The proof</p>
        <h1 class="title"><span class="line" data-line>Clean lines,</span><span class="line" data-line>set to print.</span></h1>
        <p class="text" data-soft>Cut, corrected and read twice. What is left is ready for the press.</p>
        <a class="go go--back" href="#draft" data-go="a" data-autoclic data-soft>
          <span class="go__arrow" aria-hidden="true"><svg class="go__head" viewBox="0 0 8 12"><path d="M6.5 1.5 1.5 6l5 4.5"/></svg><span class="go__shaft"></span></span>
          <span class="go__text">Back to the draft</span>
        </a>
      </div>
    </section>

    <!-- the strikes and the caret are drawn here, over the page -->
    <svg class="ink" data-ink aria-hidden="true"></svg>

    <footer class="foot">
      <span>An editing studio</span>
      <span>Read twice, print once</span>
    </footer>
  </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.

30 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 proofreader's marks transition” from Animorio and fit it into my site

What it does

The site of a made-up editing studio, Galley (a galley is the first proof in a column, the one that gets corrected), in two pages and almost nothing else: a paper-coloured background, a big title on two lines, a line of text, a link, and on the right the margin column, set off by a grey hairline, where the proofreader writes the marks in red. On load a small red caret «^» pops up on the title's baseline, the insertion mark is drawn in the margin with the word «ins», and the letters of Rough words, first thoughts. come out of the caret one after another: they start narrow and squashed, slide to their place widening, pass it by a pixel and come back on a small elastic; the caret nods at every letter, then moves down to the second line and starts again, and at the end sinks away. A click on Send it to proof sends a slightly wavering red line across the title's first line like a pen stroke, then the second; the pen lifts a pixel at the end of each stroke. In the margin the delete mark is drawn, the curl of «del». The smaller lines, the kicker and the text, are struck out with a lighter hairline. Then the struck letters leave from the last to the first, narrowing and dropping a hair, and the red line shortens behind them. Only the margin and the paper are left; the caret arrives and Clean lines, set to print. comes in the same way, while the text and the link rise in with a fade. A click on Back to the draft goes back the same way. Hovering the link widens its letters, stretches the arrow and brings up a small red caret underneath.

How it works

One letter, one span

The script splits each title line into one .ch span per letter (spaces become .sp, 0.24em wide) and keeps the whole text in aria-label for screen readers. Each .ch is an inline-block with font-stretch: calc(var(--w) * 1%): GSAP moves the span and animates the --w variable, so the letter narrows and widens on Roboto Flex's wdth axis as it moves.

The pen line

The strikes are drawn in an SVG as big as the stage, over the page. The line measures come from document.createRange().getClientRects(), which gives one box per line of text even when a paragraph wraps. Each stroke is a Bézier curve starting five pixels before the word and ending five after, with two control points moved WOBBLE pixels up and down: it looks hand drawn. It is drawn with stroke-dasharray and stroke-dashoffset, from the full length to zero.

The letters leaving

Once the line is drawn, the letters leave from the last: opacity to zero, --w down to NARROW and three hundredths of the size downwards, ERASE seconds apart with power2.in. Meanwhile the stroke's stroke-dashoffset goes from zero to its length with ease none, in the same time: with the dash as long as the path, a growing offset shortens the line from its end, so the strike follows the letters as they go.

The caret and the letters coming in

The caret is an SVG group with an upside-down V path, placed at the start of the first letter, on the baseline. It appears with scale from 0 and back.out(3). Each new letter starts at an x equal to the distance between the caret and its place, transparent, with --w at NARROW and scaleY 0.4, and reaches OVERSHOOT pixels past its place with power3.out; then it comes back to zero with elastic.out(1, 0.5). One letter every INSERT seconds; every third letter the caret dips a pixel and back. For the second line the caret slides down with power3.inOut.

Margin, direction and reduced motion

The margin marks are two SVGs whose paths are drawn with stroke-dashoffset, with the word rising on back.out; they sit level with the middle of the title. Each change is one timeline: the new page's part is added only once that page is on screen, because only there can its letters be measured. A click during the change waits in a queue. The title shrinks if its longest line does not fit the column, on every resize. With reduced motion the pages swap with a fade, with no marks and nothing moving.

Values to change

NameFileDefaultWhat it changes
STRIKEscript.js0.42The seconds the red line takes to cross one line of the title.
ERASEscript.js0.022The seconds between one deleted letter and the next: 0 = they all go at once.
INSERTscript.js0.034The seconds between one inserted letter and the next.
SETTLEscript.js0.62The seconds each new letter takes to slide out of the caret into its place.
WOBBLEscript.js1.4How many pixels the pen line wavers up and down: 0 = straight as a ruler.
NARROWscript.js25How narrow a letter gets as it leaves or is born (font-stretch in %, 25 to 100).
OVERSHOOTscript.js1How many pixels a new letter runs past its place before settling: 0 = it just stops.
SPEEDscript.js1The speed of everything: 2 is twice as fast, 0.5 half as fast.

Try them in the Playground

When to use it

To move from one page to another on a site where words matter: an editing or copywriting studio, a publisher, a magazine, a writing blog, a translation or proofreading service. It also works well to go from a draft to the final version of something, or from «before» to «after».

On phones

The same transition with the sizes adapted: the title shrinks until its longest line fits the column, and the margin narrows with its red marks. A tap on the link sends the page back and forth.

With reduced motion

Nothing moves and nothing is struck out: the old page fades out and the new one fades in; the red lines, the caret and the margin marks do not appear.

Pitfalls

  • The new page's letters can only be measured once the page is visible: the part of the timeline that brings them in is built in a callback, as soon as the page is on screen, and added to the timeline already running.
  • A wrapping paragraph has several lines: a Range's getClientRects() gives them one by one, but sometimes with doubled boxes for the same line; they are merged when they share a height.
  • To shorten the line from its end, not its start, stroke-dashoffset has to grow from zero to the stroke's length, with the dash as long as the path.
  • A click arriving halfway through the change (the previews' autopilot clicks on its own) would start on top of a timeline still running: it waits in a queue.

Libraries

GSAP 3.15.0, Roboto Flex (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 7.1 KB; the libraries: GSAP 28 KB.

Testing

Tested on 23 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 animations