Skip to content
animorio
IT
Back to the library

The scan-line transition

Transitions, TextGSAP, CSS onlyMediumCompressed weight: Demo 7.1 KBGSAP + CustomEase 31 KBFreeDownload the zip

A minimal click page change: one thin vermilion line crosses the screen like the head of a scanner. Above it the new page is already there, below it the old one is still left; the new title comes in thin and condensed and, as soon as the line has passed it, settles into width and weight on a spring, one letter after another. On the next page the line scans back up from the bottom.

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 scan-line transition</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" data-stage>
    <!-- three pages of a made-up print room; the line scans each new one over the old -->
    <section class="page page--paper" data-page="prints" aria-label="Prints" aria-hidden="true">
      <header class="bar">
        <span class="brand">Plano</span>
        <nav class="nav" aria-label="Pages">
          <a href="#prints" aria-current="page">Prints</a>
          <a href="#archive" data-go="archive">Archive</a>
          <a href="#visit" data-go="visit">Visit</a>
        </nav>
      </header>
      <div class="hero">
        <p class="kicker">Risograph print room</p>
        <h1 class="title" data-title>Prints</h1>
        <p class="lede">Two drums, eleven inks and a great deal of paper. Small runs, printed by hand.</p>
        <a class="go" href="#archive" data-go="archive" data-autoclic>
          <span class="go__text">See the archive</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>
      <footer class="foot">
        <span>Plano print room</span>
        <span>Open Thursday to Saturday</span>
      </footer>
    </section>

    <section class="page page--night" data-page="archive" aria-label="Archive" aria-hidden="true">
      <header class="bar">
        <span class="brand">Plano</span>
        <nav class="nav" aria-label="Pages">
          <a href="#prints" data-go="prints">Prints</a>
          <a href="#archive" aria-current="page">Archive</a>
          <a href="#visit" data-go="visit">Visit</a>
        </nav>
      </header>
      <div class="hero">
        <p class="kicker">Since 2014</p>
        <h2 class="title" data-title>Archive</h2>
        <p class="lede">Every run we have printed, one copy of each, kept flat in wide drawers.</p>
        <a class="go" href="#visit" data-go="visit" data-autoclic>
          <span class="go__text">Plan a visit</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>
      <footer class="foot">
        <span>Plano print room</span>
        <span>Open Thursday to Saturday</span>
      </footer>
    </section>

    <section class="page page--stone" data-page="visit" aria-label="Visit" aria-hidden="true">
      <header class="bar">
        <span class="brand">Plano</span>
        <nav class="nav" aria-label="Pages">
          <a href="#prints" data-go="prints">Prints</a>
          <a href="#archive" data-go="archive">Archive</a>
          <a href="#visit" aria-current="page">Visit</a>
        </nav>
      </header>
      <div class="hero">
        <p class="kicker">Thursday to Saturday</p>
        <h2 class="title" data-title>Visit</h2>
        <p class="lede">Bring a file on a stick, leave with a warm stack of paper. The coffee is on us.</p>
        <a class="go" href="#prints" data-go="prints" data-autoclic>
          <span class="go__text">Back to prints</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>
      <footer class="foot">
        <span>Plano print room</span>
        <span>Open Thursday to Saturday</span>
      </footer>
    </section>

    <!-- the scan line: a hairline, a faint wash behind it that grows with its speed, a tick at each end -->
    <div class="scan" data-scan aria-hidden="true">
      <div class="scan__trail" data-trail></div>
      <div class="scan__rule" data-rule></div>
      <div class="scan__tick scan__tick--l" data-tick></div>
      <div class="scan__tick scan__tick--r" data-tick></div>
    </div>
  </main>

  <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/CustomEase.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.

45 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 scan-line transition” from Animorio and fit it into my site

What it does

The site of a made-up risograph print room, Plano, in three pages and almost nothing else: a huge title in Archivo, a line of text, a link with a drawn arrow, and a vermilion line one and a half pixels thick. On load the line starts at the top edge and runs down: above it the first page appears, below it the empty sheet is still left. It starts slowly, runs through the middle and brakes gently into the bottom edge, where it rests with its two ticks, which stretch and come back with a little spring. Behind the line, on the side already read, a faint tinted wash grows while it runs and fades as it brakes. The title, Prints, comes in very thin and condensed (width 62, weight 160): as soon as the line has passed under it, the letters widen and gain weight on a spring, left to right, sinking a pixel and coming back up. A click on See the archive sends the line back up from the bottom: below it the black archive page rises, above it the paper page is still left, and Archive settles as soon as the line has passed the top of its capitals. The next click runs the line down again with the stone-grey Visit page, and the one after brings it back up to Prints, like a scanner head going back and forth.

How it works

One line and one clip

The new page sits on top of the old one (higher z-index) and is clipped with clip-path: going down inset(0 0 calc(height - y) 0), going up inset(y 0 0 0), where y is the line's position. The line is a zero-height div holding the one-and-a-half-pixel rule, the two ticks and the wash: it only moves in y, and on every frame the line and the clip are written together from the same y, so they never come apart.

Speeding up and braking

The run is a tween of a number from 0 to 1 with a linear ease; y is worked out by passing that number through the SWEEP curve, a CustomEase (0.66, 0, 0.16, 1: starts slowly, runs, brakes for a long time). So the speed comes from the same curve: its slope at that point, divided by the highest speed, gives a number from 0 to 1 that stretches the wash behind the line (scaleY) while it runs and fades it as it brakes.

The title that settles

Each title is split into letters, inline-blocks whose font-stretch and font-weight are tied to the --w and --g variables (Archivo runs from 62 to 125 in width and from 100 to 900 in weight; clamp in the CSS keeps them inside even when the spring carries them past). Before the run the letters go to FROM_WIDTH and FROM_WEIGHT; then a tween with elastic.out(1, SPRING) takes them to 100 and 620, staggered by STAGGER, while they sink a pixel and a half and come back.

The right moment

The title does not wait for a check on every frame: the moment the line passes it is worked out beforehand, by bisecting the curve for the point where the line reaches the title's baseline (going down) or the top of its capitals (going up), and the tween is placed there in the timeline. So the editing bench can slow it, stop it and rewind it without missing the beat.

Back and forth, phone and reduced motion

The line always starts from the edge it rests on: after a run down it stays at the bottom and the next page comes up, like a scanner head (with BACK_AND_FORTH set to false it fades back to the top and always runs down). The three titles share one size, the one that fits the longest in the width, and sit at the same height on every page. A click during a change waits in a queue. With reduced motion the new page fades in with its title already in place and the line changes edge in a fade.

Values to change

NameFileDefaultWhat it changes
SWEEP_TIMEscript.js1.35The seconds the line takes to cross the page.
SWEEPscript.js0.66, 0, 0.16, 1The curve of the run: how the line starts, runs and brakes.
FROM_WIDTHscript.js62The title's width before the line passes it, 62 to 125 (it settles at 100): above 100 it comes in wide and narrows.
FROM_WEIGHTscript.js160The title's weight before the line passes it, 100 to 900 (it settles at 620).
SPRINGscript.js0.42The spring of the settle: lower wobbles more, 1 hardly wobbles.
STAGGERscript.js0.04The seconds between one letter settling and the next: 0 = all together.
TRAILscript.js90The pixels of the wash behind the line at full speed: 0 = no wash.
BACK_AND_FORTHscript.jstrueOn, the next page is scanned back up from the bottom; off, the line always runs down from the top.

Try them in the Playground

When to use it

To move from one page to another on a quiet site where the type does all the work: a print shop, a design studio, an archive, a portfolio, a magazine, an agency's about page. The line reading the page suits anything about paper, print, scans and documents.

On phones

The same transition with the sizes adapted: the titles shrink to fit the width of the screen, sit a little lower on the tall page and the text wraps. A tap on the link sends the line back and forth.

With reduced motion

Nothing moves: the new page fades in over the old one with its title already settled, and the line fades out at one edge and back in at the other.

Pitfalls

  • The title is an h1, a block as wide as the column: measuring it with scrollWidth gives the column, not the word, and the titles came out small. The word is measured from the first letter to the last, with getBoundingClientRect.
  • With the spring, width and weight swing past their target, and past 125 or 900 the typeface has no more shapes: in the CSS font-stretch and font-weight go through clamp().
  • The letters are inline-blocks and as they change width they push their neighbours: all three titles are measured with the letters settled (--w 100, --g 620), before they are set to the thin shape.
  • Checking on every frame whether the line has passed the title only works going forwards: with the bench rewinding or skipping, the beat was lost. The moment is worked out beforehand by bisecting the curve and the tween goes into the timeline.
  • The previews' autopilot clicks every 2.8 seconds and finds marked links in hidden pages too: each page has its own next link with data-autoclic, in order, so the clicks go round the three pages; a click during a change waits in a queue.

Libraries

GSAP 3.15.0, CustomEase (GSAP) 3.15.0, Archivo (font variabile) 5.3.0.

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

Testing

Tested on 15 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