
Words that melt into each other
Text, Scroll, SVG
NewFree
Scroll inside the preview
A coloured wipe rises over the page; between two pairs of quote marks a black frame as thin as a line opens and pushes them apart, a choir rises into the frame and the closing marks slide to their place, uncovering the quote.
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 marks that make room</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/big-shoulders@5.3.0/standard.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/newsreader@5.3.0/standard.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/standard.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- the quote mark, drawn once and used four times -->
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="mark" viewBox="0 0 100 190">
<path d="M50 90a50 50 0 1 1 0 100a50 50 0 1 1 0-100ZM0 140C0 72 22 24 64 0l12 14C50 38 40 72 44 104Z" />
</symbol>
</svg>
<header class="top">
<p class="brand">Lanterna Philharmonic</p>
<p class="top__line">The chorus</p>
</header>
<main>
<!-- one tall section with a stage that stays on screen: the page before, and the coloured wipe that rises over it -->
<section class="quote" data-quote>
<div class="quote__stage">
<div class="before" data-before>
<h1 class="before__title"><span>Sing</span> <span>along</span></h1>
<p class="before__text">Every Thursday the chorus rehearses with the doors open. Bring your voice, or just come and listen.</p>
<p class="before__hint">Scroll</p>
</div>
<div class="wipe" data-wipe>
<div class="row" data-row>
<div class="marks" data-open aria-hidden="true">
<svg class="mark" viewBox="0 0 100 190"><use href="#mark" /></svg>
<svg class="mark" viewBox="0 0 100 190"><use href="#mark" /></svg>
</div>
<div class="cutout" aria-hidden="true">
<div class="cutout__frame" data-frame></div>
<div class="cutout__choir" data-choir>
<svg viewBox="0 0 1100 540" aria-hidden="true">
<g transform="translate(34 0)">
<!-- back row -->
<g class="singer">
<path d="M58 540c4-128 52-196 132-204 80 8 128 76 132 204Z" fill="#9ad6ff" />
<path d="M150 342l40 50 40-50Z" fill="#fff3da" />
<rect x="172" y="282" width="36" height="64" rx="14" fill="#8a5a3c" />
<ellipse cx="190" cy="232" rx="66" ry="74" fill="#a86c46" />
<path d="M120 236c-10-76 36-122 88-116 50 6 80 50 66 118-10-40-40-62-80-60-36 2-62 22-74 58Z" fill="#20160f" />
<path d="M160 238q10 8 20 0M202 238q10 8 20 0" fill="none" stroke="#20160f" stroke-width="5" stroke-linecap="round" />
<ellipse class="mouth" cx="191" cy="272" rx="11" ry="14" fill="#3b1a12" />
</g>
<g class="singer">
<path d="M318 540c4-136 54-208 138-216 84 8 134 80 138 216Z" fill="#ff5a36" />
<path d="M414 330l42 46 42-46Z" fill="#fff3da" />
<rect x="437" y="268" width="38" height="64" rx="14" fill="#e3a47c" />
<ellipse cx="456" cy="214" rx="68" ry="76" fill="#f2bb93" />
<circle cx="456" cy="118" r="34" fill="#6b3b1f" />
<path d="M386 222c-8-70 30-104 70-104s80 34 70 104c-8-34-34-52-70-52s-62 18-70 52Z" fill="#6b3b1f" />
<path d="M424 222q10 8 20 0M468 222q10 8 20 0" fill="none" stroke="#3a2012" stroke-width="5" stroke-linecap="round" />
<ellipse class="mouth" cx="457" cy="256" rx="12" ry="15" fill="#5a2418" />
</g>
<g class="singer">
<path d="M584 540c4-130 52-198 132-206 80 8 128 76 132 206Z" fill="#fff3da" />
<path d="M676 340l40 48 40-48Z" fill="#10132e" />
<rect x="698" y="280" width="36" height="64" rx="14" fill="#5e3a26" />
<ellipse cx="716" cy="228" rx="64" ry="74" fill="#704630" />
<path d="M646 244c-18-44-8-104 36-122 22-8 50-8 70 2 44 20 50 80 30 120 2-38-22-66-66-66s-70 28-70 66Z" fill="#161616" />
<circle cx="662" cy="160" r="22" fill="#161616" /><circle cx="702" cy="132" r="24" fill="#161616" /><circle cx="746" cy="140" r="24" fill="#161616" /><circle cx="776" cy="176" r="20" fill="#161616" />
<path d="M688 236q10 8 20 0M728 236q10 8 20 0" fill="none" stroke="#161616" stroke-width="5" stroke-linecap="round" />
<ellipse class="mouth" cx="717" cy="270" rx="11" ry="14" fill="#2e130c" />
</g>
<!-- front row -->
<g class="singer">
<path d="M196 540c4-104 44-160 110-166 66 6 106 62 110 166Z" fill="#ffb81c" />
<path d="M274 380l32 40 32-40Z" fill="#10132e" />
<rect x="290" y="330" width="32" height="56" rx="12" fill="#9c6340" />
<ellipse cx="306" cy="286" rx="58" ry="64" fill="#b87650" />
<path d="M246 296c-10-60 22-94 62-94 36 0 66 26 60 86-14-26-34-40-62-40-28 0-48 18-60 48Z" fill="#2a1a12" />
<path d="M278 292q9 7 18 0M316 292q9 7 18 0" fill="none" stroke="#2a1a12" stroke-width="5" stroke-linecap="round" />
<ellipse class="mouth" cx="307" cy="322" rx="10" ry="12" fill="#3b1a12" />
</g>
<g class="singer">
<path d="M466 540c4-108 46-166 114-172 68 6 110 64 114 172Z" fill="#10132e" />
<path d="M546 372l34 42 34-42Z" fill="#ffb81c" />
<rect x="562" y="322" width="36" height="58" rx="12" fill="#f0c6a2" />
<ellipse cx="580" cy="276" rx="60" ry="66" fill="#fbd6b4" />
<path d="M516 300c-18-72 18-110 64-110 50 0 82 40 64 110-4-40-26-66-64-66s-60 26-64 66Z" fill="#d9772e" />
<path d="M552 282q9 7 18 0M590 282q9 7 18 0" fill="none" stroke="#6b3a1c" stroke-width="5" stroke-linecap="round" />
<ellipse class="mouth" cx="581" cy="312" rx="10" ry="13" fill="#7a2c1c" />
</g>
<g class="singer">
<path d="M740 540c4-102 44-158 110-164 66 6 106 62 110 164Z" fill="#ff5a36" />
<path d="M818 382l32 40 32-40Z" fill="#fff3da" />
<rect x="834" y="330" width="32" height="56" rx="12" fill="#4f301f" />
<ellipse cx="850" cy="286" rx="58" ry="64" fill="#5f3a27" />
<path d="M790 280c0-48 26-76 60-76s60 28 60 76c-12-18-32-28-60-28s-48 10-60 28Z" fill="#101010" />
<path d="M822 292q9 7 18 0M860 292q9 7 18 0" fill="none" stroke="#101010" stroke-width="5" stroke-linecap="round" />
<ellipse class="mouth" cx="851" cy="322" rx="10" ry="12" fill="#2a0f0a" />
</g>
</g>
</svg>
</div>
</div>
<blockquote class="copy" data-copy>
<p>A city sounds best when everyone gets to sing along, even the ones who never learned the words.</p>
<footer>The Lanterna Chorus, rehearsal notes</footer>
</blockquote>
<div class="marks marks--close" data-close aria-hidden="true">
<svg class="mark" viewBox="0 0 100 190"><use href="#mark" /></svg>
<svg class="mark" viewBox="0 0 100 190"><use href="#mark" /></svg>
</div>
</div>
</div>
</div>
</section>
<section class="outro">
<p class="outro__text">Thursdays at seven, in the small hall.</p>
</section>
</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/ScrollTrigger.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.
23 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 marks that make room” from Animorio and fit it into my siteThe section is 4.6 screens tall and its stage stays put with position: sticky. Inside are the page before and the wipe, which covers it completely. One timeline, tied to the scroll until 85% of the section reaches the bottom of the screen, raises the wipe with clip-path from inset(100% 0 0 0) to inset(0) and meanwhile moves the page underneath up by 12%.
Opening marks, frame, quote and closing marks are a grid of four columns. The marks are a single SVG symbol, a ball with a tail, used four times; the closing ones are the same turned by 180°. Everything is measured on the screen’s width: marks and frame are 11vw tall, the frame two and a half times as wide.
The distances are taken with the frame wide, then it is narrowed to 10% and both pairs of marks are set against it. Halfway up the wipe the frame widens again in 0.7: the opening marks go back to their place, the closing ones go right after the frame. The closing marks carry an amber block as long as the screen, which lies on the quote and hides it; at the end they slide to their place and uncover it from left to right.
An SVG drawing of six singers, wider than the frame, rises from below (yPercent from 100 to 0). The frame cuts it with a polygon: at its own height it cuts on its edges, above it lets the heads come out, and a little to the sides too. The mouths open and close with GSAP, each at its own pace, so they follow the slow motion as well.
| Name | File | Default | What it changes |
|---|---|---|---|
START | script.js | 0.1 | How wide the frame is at first, as a share of its width. |
OPEN_AT, OPEN | script.js | 0.5, 0.7 | When the frame starts to open and how long it takes (the wipe lasts 1). |
CHOIR_AT, REVEAL_AT | script.js | 0.82, 1.13 | When the choir rises and when the closing marks uncover the quote. |
DRIFT | script.js | -12 | How far the page before moves up while the wipe covers it, in percent of its height. |
--bar, --quote-gap | style.css | 11vw, 0.9vw | The height of the marks and the frame, and the space between the two marks of each pair. |
For a quote that has to weigh: a founder’s sentence, a review, the voice of someone who uses a product, with a cut-out photo or a drawing in place of the choir.
One column: opening marks on top, a frame as wide as the screen, the quote, closing marks at the bottom right. The frame opens from the centre, the choir rises and the marks and the quote fade in.
No wipe and no pushing: the page before and then the quote, already open, with the choir still.
GSAP 3.15.0, ScrollTrigger (GSAP) 3.15.0, Big Shoulders (font variabile) 5.3.0, Newsreader (font variabile) 5.3.0, Mona Sans (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 5.9 KB; the libraries: GSAP + ScrollTrigger 45 KB.
Tested on 29 September 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.