Skip to content
animorio
IT
Back to the library

The quote that lights up

Scroll, TextGSAP, CSS onlyMediumCompressed weight: Demo 7.8 KBGSAP + ScrollTrigger 45 KBLenis 5.3 KBFreeDownload the zip

Scroll inside the preview

Halfway through an interview a large serif quote holds still on screen: as you scroll the quotation marks draw themselves, the words go from pale grey to black one after another, then the portrait opens in a circle and a thin rule closes it.

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 quote that lights up</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/newsreader@5.3.0/opsz.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/newsreader@5.3.0/opsz-italic.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="testata">
    <p class="testata__nome">Halden Quarterly</p>
    <p class="testata__numero">Issue 14 <span aria-hidden="true">&middot;</span> Autumn 2026</p>
  </header>

  <main>
    <article class="articolo">
      <header class="apertura">
        <p class="occhiello">Interview <span aria-hidden="true">/</span> Painting</p>
        <h1 class="titolo">Ines Vadell paints the minute before you look away</h1>
        <p class="catenaccio">For thirty years the painter has worked from the same window above the river. We talked about patience, bad weather and the colour of waiting.</p>
        <p class="firma-pezzo">Interview by <strong>Theo Marchetti</strong><span class="firma-pezzo__sep" aria-hidden="true"></span>8 min read</p>
      </header>

      <div class="testo">
        <p class="domanda">You have painted the same view since 1994. Do you ever get bored of it?</p>
        <p class="capolettera">Bored, never. Tired, often. The view is never the same view. In March the river is the colour of pewter, and by May it has turned to something like milk with a little green in it. People think a window is a frame. For me it is a clock.</p>
        <p class="domanda">So what are you actually looking for when you sit down to work?</p>
        <p>Not the object. The object is easy, anyone can draw a boat. I wait for the change, the moment the weather decides something. Most days nothing happens, and I put the brushes back in the jar and make soup.</p>
      </div>
    </article>

    <!-- the run: a tall section; its stage stays on screen while the words light up one after another -->
    <section class="corsa" data-corsa>
      <div class="palco">
        <figure class="citazione">
          <svg class="virgolette" viewBox="0 0 74 52" aria-hidden="true">
            <g transform="rotate(180 15.5 25.5)"><path class="virgoletta" data-virgoletta d="M15 1A12 12 0 0 1 27 13C27 30 19 42 6 49L4 46C12 40 17 32 17 25A12 12 0 1 1 15 1Z" /></g>
            <g transform="translate(40 0) rotate(180 15.5 25.5)"><path class="virgoletta" data-virgoletta d="M15 1A12 12 0 0 1 27 13C27 30 19 42 6 49L4 46C12 40 17 32 17 25A12 12 0 1 1 15 1Z" /></g>
          </svg>

          <blockquote class="frase">
            <p data-frase>I don&rsquo;t paint the river. I paint the light that leaves it, a second before anyone notices.</p>
          </blockquote>

          <figcaption class="autrice" data-autrice>
            <span class="ritratto" data-ritratto>
              <svg viewBox="0 0 200 200" aria-hidden="true" data-ritratto-foto>
                <defs>
                  <radialGradient id="r-fondo" cx="22%" cy="30%" r="95%">
                    <stop offset="0" stop-color="#8a7a66" />
                    <stop offset="0.38" stop-color="#4a3f35" />
                    <stop offset="1" stop-color="#16120f" />
                  </radialGradient>
                  <linearGradient id="r-pelle" x1="0" y1="0" x2="1" y2="0.2">
                    <stop offset="0" stop-color="#e7c3a2" />
                    <stop offset="0.45" stop-color="#a57a5d" />
                    <stop offset="1" stop-color="#3a2a21" />
                  </linearGradient>
                  <linearGradient id="r-capelli" x1="0" y1="0" x2="1" y2="0.4">
                    <stop offset="0" stop-color="#6b5444" />
                    <stop offset="0.5" stop-color="#2e231d" />
                    <stop offset="1" stop-color="#140f0c" />
                  </linearGradient>
                  <linearGradient id="r-maglia" x1="0" y1="0" x2="1" y2="0.5">
                    <stop offset="0" stop-color="#8d8577" />
                    <stop offset="0.4" stop-color="#4b463f" />
                    <stop offset="1" stop-color="#1b1916" />
                  </linearGradient>
                  <radialGradient id="r-vignetta" cx="40%" cy="42%" r="70%">
                    <stop offset="0.55" stop-color="#000" stop-opacity="0" />
                    <stop offset="1" stop-color="#000" stop-opacity="0.55" />
                  </radialGradient>
                  <filter id="r-morbido" x="-10%" y="-10%" width="120%" height="120%"><feGaussianBlur stdDeviation="0.8" /></filter>
                  <clipPath id="r-testa"><path d="M108 30C92 30 80 38 76 52C73 60 73 66 72 70C71 74 70 76 70 79C67 84 63 88 61 92C60 94 62 96 65 96C67 97 68 98 67 100C66 102 67 104 68 105C67 107 67 109 68 110C69 112 71 113 71 115C70 118 69 121 71 124C74 129 82 131 90 132L92 158L134 155C131 140 132 126 136 116C146 96 144 60 132 42C126 34 117 30 108 30Z" /></clipPath>
                  <filter id="r-ombra" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="5" /></filter>
                  <filter id="r-luce" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="9" /></filter>
                  <filter id="r-grana" x="0" y="0" width="100%" height="100%">
                    <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="4" result="n" />
                    <feColorMatrix in="n" type="saturate" values="0" />
                  </filter>
                </defs>
                <rect width="200" height="200" fill="url(#r-fondo)" />
                <!-- the window light falling on the wall behind her -->
                <ellipse cx="34" cy="62" rx="44" ry="70" fill="#d9c7ad" opacity="0.35" filter="url(#r-luce)" />
                <g filter="url(#r-morbido)">
                  <!-- shoulders and sweater -->
                  <path d="M4 200C8 178 30 164 60 158C74 155 86 153 92 152L134 150C150 152 172 160 186 174C194 182 198 192 200 200Z" fill="url(#r-maglia)" />
                  <!-- head and neck in profile, facing the window -->
                  <path d="M108 30C92 30 80 38 76 52C73 60 73 66 72 70C71 74 70 76 70 79C67 84 63 88 61 92C60 94 62 96 65 96C67 97 68 98 67 100C66 102 67 104 68 105C67 107 67 109 68 110C69 112 71 113 71 115C70 118 69 121 71 124C74 129 82 131 90 132L92 158L134 155C131 140 132 126 136 116C146 96 144 60 132 42C126 34 117 30 108 30Z" fill="url(#r-pelle)" />
                  <!-- the side of the face away from the light, and the shadow of the jaw on the neck -->
                  <ellipse cx="128" cy="100" rx="30" ry="46" fill="#1a120d" opacity="0.6" filter="url(#r-ombra)" clip-path="url(#r-testa)" />
                  <path d="M88 132C100 140 118 138 134 124L134 150L92 152Z" fill="#1a120d" opacity="0.7" filter="url(#r-ombra)" />
                  <!-- hair pulled back into a low knot -->
                  <ellipse cx="150" cy="98" rx="17" ry="16" fill="url(#r-capelli)" />
                  <path d="M78 52C82 40 96 31 111 30C128 29 142 40 147 57C151 72 149 92 143 108C140 116 137 121 134 126C131 114 127 104 121 96C114 88 110 78 104 69C97 60 88 56 78 52Z" fill="url(#r-capelli)" />
                  <path d="M82 47C92 37 106 33 118 34" stroke="#b4957a" stroke-width="1.6" fill="none" opacity="0.5" stroke-linecap="round" />
                  <path d="M92 50C104 46 118 50 128 60" stroke="#8d725e" stroke-width="1" fill="none" opacity="0.45" stroke-linecap="round" />
                  <!-- the collar of the sweater -->
                  <path d="M88 152C100 160 124 160 138 150L140 156C126 166 100 166 86 158Z" fill="#2c2924" opacity="0.8" />
                </g>
                <!-- rim of light along the profile -->
                <path d="M108 30C92 30 80 38 76 52C73 60 73 66 72 70C71 74 70 76 70 79C67 84 63 88 61 92C60 94 62 96 65 96C67 97 68 98 67 100C66 102 67 104 68 105C67 107 67 109 68 110C69 112 71 113 71 115C70 118 69 121 71 124C74 129 82 131 90 132" stroke="#fff3e2" stroke-width="1.4" fill="none" opacity="0.5" filter="url(#r-morbido)" />
                <rect width="200" height="200" fill="url(#r-vignetta)" />
                <rect width="200" height="200" filter="url(#r-grana)" opacity="0.12" style="mix-blend-mode: overlay" />
              </svg>
            </span>
            <span class="autrice__testo">
              <span class="autrice__riga"><cite class="autrice__nome" data-nome>Ines Vadell</cite></span>
              <span class="autrice__riga"><span class="autrice__ruolo" data-ruolo>Painter, in her studio above the river</span></span>
            </span>
          </figcaption>
        </figure>
        <span class="filetto" data-filetto aria-hidden="true"></span>
      </div>
    </section>

    <article class="articolo articolo--seguito">
      <div class="testo">
        <p class="domanda">Is that why the paintings take so long?</p>
        <p>Some take a week, some take eleven years. I keep them leaning against the wall with their faces turned in, like children who are being punished. When I turn one around and it surprises me, it is finished.</p>
        <p class="domanda">What would you tell someone who wants to start?</p>
        <p>Find one window and stay with it. Buy less paint than you think you need. And learn to enjoy the days when the light refuses to come, because those are most of the days.</p>
        <p class="nota">This interview has been edited and condensed. New paintings by Ines Vadell are on view at Galeria Atlântico until February.</p>
      </div>
    </article>
  </main>

  <footer class="piede">
    <p class="testata__nome">Halden Quarterly</p>
    <p class="piede__riga">Next in this issue <span class="piede__titolo">The last bell foundry in the valley</span></p>
  </footer>

  <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="https://cdn.jsdelivr.net/npm/lenis@1.3.26/dist/lenis.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.

43 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 quote that lights up” from Animorio and fit it into my site

What it does

An interview in an invented magazine: kicker, headline, standfirst, byline, questions and answers. Halfway down the page stops on a large quote, set in Newsreader, all in pale grey. As you scroll, the brick-red quotation marks draw their outline and fill with ink with a small settle; then the words light up one after another: they turn black, rise by a tenth of an em and gain a little weight, without moving the others. When the whole sentence is lit the artist's portrait opens in a circle while the photo settles, name and caption rise from their lines and a thin rule runs out underneath. Then the page scrolls on. Going back up, everything goes out in reverse.

How it works

A stage that holds still

The quote lives in a section RUN screens plus one tall, with a 100lvh sticky stage inside: while you scroll the section, the stage stays put and a single scrubbed timeline (start top top, end bottom bottom) does everything. Lenis, driven by the GSAP clock, makes the scrolling smooth.

One word, one span

The script walks the quote's text nodes and wraps every word in an inline-block span (spans already there survive). Screen readers get the whole sentence in aria-label and the words are aria-hidden.

One variable per word

GSAP only animates --p, from 0 to 1. CSS derives everything from it: the colour with color-mix between --spento and --pieno, the weight between --peso-spento and --peso-pieno, the rise with translateY. So the colours stay in :root and change without touching the script.

Weight that does not move the lines

A heavier font is wider, and the line would shift. Before starting, the script measures every word at the heavier weight and fixes its width there: the word grows inside its centred box and the others stay still. The measurement runs again when the fonts arrive and when the window changes.

How many at once, then the signature

Each word lasts TOGETHER steps and the next one starts a step later: with 1 they light up one at a time, with 4 a soft wave goes by. First the quotation marks (stroke-dashoffset then fill-opacity, with a back.out), then the portrait (clip-path circle from 0 to 50%), the two signature lines and the rule with scaleX.

Values to change

NameFileDefaultWhat it changes
TOGETHERscript.js2.5How many words are lighting up at the same moment: 1 is one at a time, 4 is a soft wave.
RISEscript.js0.1How far each word rises while it lights up, in em.
RUNscript.js3.2For how many screens of scrolling the quote holds still: the longer, the slower the reading.
SMOOTHscript.js0.8How much the animation trails the scroll, in seconds (0 = glued to it).
--spentostyle.css#d9d3c9The colour of the words not yet lit.
--pienostyle.css#16130fThe colour of the lit words.
--peso-pienostyle.css460The font weight of a lit word (the unlit one is --peso-spento, 380).

Try them in the Playground

When to use it

For the sentence that has to stay with the reader in a long article, a brand story or a project page: a quote, a principle, a promise. Two or three lines, no more: on a whole paragraph the scroll becomes very long.

On phones

The same animations: the quote goes down to 34 pixels over four lines, the quotation marks and the portrait get smaller, the run is a little shorter.

With reduced motion

The words are already black and still; the quotation marks, the signature with the portrait and the rule only fade in, and the section is short.

Pitfalls

  • Changing the weight of a variable font changes its width: without the width fixed at the heavier weight, every word that lights up pushes the others and sometimes sends a word to the next line. Measure after document.fonts.ready, or you measure the fallback font.
  • With boxes as wide as the heavy word, the unlit words look more widely spaced: that is why the weight jump is small (380 and 460). A big jump shows in the gaps between words.
  • GSAP writes its own inline transform and wipes the CSS one: here GSAP only touches --p and the rise is done by CSS with translateY. If you animated y with GSAP on the same word, the variable and the tween would trip over each other.
  • The quotation marks already have a transform in the SVG (they are rotated 180 degrees): GSAP would replace it. The rotation sits on the g group, GSAP only animates the path inside.
  • The sticky stage is 100lvh tall, not 100svh: on iPhone Safari, when the toolbar hides, 100svh would leave a strip and the stage would jerk.

Libraries

GSAP 3.15.0, ScrollTrigger (GSAP) 3.15.0, Lenis 1.3.26, Newsreader (font variabile) 5.3.0, Instrument Sans (font variabile) 5.3.0.

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

Testing

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