
Standing letters with a long shadow
Text, Scroll, 3D
NewFree
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.
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">·</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’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>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
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 siteThe 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.
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.
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.
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.
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.
| Name | File | Default | What it changes |
|---|---|---|---|
TOGETHER | script.js | 2.5 | How many words are lighting up at the same moment: 1 is one at a time, 4 is a soft wave. |
RISE | script.js | 0.1 | How far each word rises while it lights up, in em. |
RUN | script.js | 3.2 | For how many screens of scrolling the quote holds still: the longer, the slower the reading. |
SMOOTH | script.js | 0.8 | How much the animation trails the scroll, in seconds (0 = glued to it). |
--spento | style.css | #d9d3c9 | The colour of the words not yet lit. |
--pieno | style.css | #16130f | The colour of the lit words. |
--peso-pieno | style.css | 460 | The font weight of a lit word (the unlit one is --peso-spento, 380). |
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.
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.
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.
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.
Tested on 12 October 2026
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.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.