
Marquee that speeds up with scroll
Scroll, Text, Hover
Lifetime
A large headline with three pills between its words: they open one after the other pushing the text apart, and passing over them they widen further, lighten and show where they lead.
A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN.
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Le pillole nel titolo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/standard.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/instrument-serif@5.3.0/index.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="pagina">
<p class="nota" data-nota>Nel titolo si aprono tre pillole con dentro una scena; passandoci sopra si allargano, si schiariscono e dicono dove portano.</p>
<h1 class="titolo" data-titolo>
Disegniamo <a class="pillola" href="#" data-pillola style="--velo: #DCEFF8"><span class="pillola__quadro"><svg viewBox="0 0 160 100" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><rect width="160" height="100" fill="#CFE6F2"/><circle cx="126" cy="26" r="13" fill="#F6D06B"/><path d="M0 58Q40 50 80 56T160 54V100H0Z" fill="#9C6B45"/><path d="M0 70H160M0 82H160M0 94H160" stroke="#7E5334" stroke-width="3"/><circle cx="12" cy="66" r="8" fill="#86C36B"/><circle cx="34" cy="78" r="8" fill="#6FAE57"/><circle cx="56" cy="66" r="8" fill="#86C36B"/><circle cx="78" cy="78" r="8" fill="#6FAE57"/><circle cx="100" cy="66" r="8" fill="#86C36B"/><circle cx="122" cy="78" r="8" fill="#6FAE57"/><circle cx="144" cy="66" r="8" fill="#86C36B"/></svg></span><span class="pillola__nome">Chi siamo</span></a> orti e giardini che si guardano <a class="pillola" href="#" data-pillola style="--velo: #FBE3EC"><span class="pillola__quadro"><svg viewBox="0 0 160 100" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><rect width="160" height="100" fill="#F7E3D6"/><rect y="70" width="160" height="30" fill="#7FA35C"/><path d="M16 70V44" stroke="#4E7A3A" stroke-width="3"/><path d="M10 44Q16 28 22 44Q16 52 10 44Z" fill="#E8637A"/><path d="M42 70V50" stroke="#4E7A3A" stroke-width="3"/><path d="M36 50Q42 34 48 50Q42 58 36 50Z" fill="#F2A23C"/><path d="M68 70V56" stroke="#4E7A3A" stroke-width="3"/><path d="M62 56Q68 40 74 56Q68 64 62 56Z" fill="#B78BE0"/><path d="M94 70V44" stroke="#4E7A3A" stroke-width="3"/><path d="M88 44Q94 28 100 44Q94 52 88 44Z" fill="#E8637A"/><path d="M120 70V50" stroke="#4E7A3A" stroke-width="3"/><path d="M114 50Q120 34 126 50Q120 58 114 50Z" fill="#F2A23C"/><path d="M146 70V56" stroke="#4E7A3A" stroke-width="3"/><path d="M140 56Q146 40 152 56Q146 64 140 56Z" fill="#B78BE0"/></svg></span><span class="pillola__nome">Lavori</span></a> anche dalla finestra <a class="pillola" href="#" data-pillola style="--velo: #E9F4D8"><span class="pillola__quadro"><svg viewBox="0 0 160 100" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"><rect width="160" height="100" fill="#EDE3D2"/><rect x="36" y="10" width="88" height="70" fill="#BFE0F0"/><circle cx="104" cy="30" r="9" fill="#FCE7A8"/><path d="M36 10H124V80H36Z M80 10V80 M36 45H124" fill="none" stroke="#FFFFFF" stroke-width="5"/><rect x="28" y="80" width="104" height="8" fill="#C9A77E"/><path d="M60 80H76L73 66H63Z" fill="#C8553D"/><ellipse cx="62" cy="60" rx="7" ry="11" fill="#5E9C52" transform="rotate(-24 62 60)"/><ellipse cx="74" cy="58" rx="7" ry="12" fill="#6FAE57" transform="rotate(20 74 58)"/></svg></span><span class="pillola__nome">Scrivici</span></a> di casa.
</h1>
</main>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="script.js"></script>
</body>
</html>/* Le pillole nel titolo */
:root {
--carta: #EEF1E8;
--inchiostro: #1F2A1C;
--tenue: #5A6655;
--margine: clamp(18px, 4vw, 56px);
--scorre: cubic-bezier(0.23, 1, 0.32, 1);
--alto: 100vh;
}
@supports (height: 100svh) {
:root { --alto: 100svh; }
}
* { box-sizing: border-box; }
html,
body {
margin: 0;
}
body {
background: var(--carta);
color: var(--inchiostro);
font-family: 'Mona Sans Variable', system-ui, sans-serif;
}
.pagina {
position: relative;
display: grid;
place-items: center;
min-height: var(--alto);
padding: calc(var(--margine) * 2.4) var(--margine) var(--margine);
}
.nota {
position: absolute;
top: var(--margine);
left: var(--margine);
max-width: 36ch;
margin: 0;
color: var(--tenue);
font-size: clamp(0.92rem, 0.84rem + 0.3vw, 1.08rem);
font-weight: 460;
line-height: 1.38;
}
.titolo {
max-width: 15ch;
margin: 0;
font-family: 'Instrument Serif', Georgia, serif;
font-size: clamp(2.8rem, 1.2rem + 6.2vw, 7.2rem);
font-weight: 400;
letter-spacing: -0.01em;
line-height: 1.04;
text-align: center;
}
/* ---- la pillola: parte larga zero, si apre e spinge le parole ---- */
.pillola {
--largo: 1.45em;
position: relative;
display: inline-block;
width: 0;
height: 0.8em;
margin: 0;
overflow: hidden;
border-radius: 999px;
color: var(--inchiostro);
text-decoration: none;
vertical-align: -0.08em;
transition: width 1200ms var(--scorre), margin 1200ms var(--scorre), transform 160ms var(--scorre);
transition-delay: calc(var(--i, 0) * 180ms);
-webkit-tap-highlight-color: transparent;
}
.titolo--aperto .pillola {
width: var(--largo);
margin: 0 0.12em;
}
.pillola__quadro {
position: absolute;
inset: 0;
}
.pillola__quadro svg {
display: block;
width: 100%;
height: 100%;
transform: scale(1.12);
}
/* il velo: un pastello che schiarisce la scena, e il nome sopra */
.pillola::after {
content: '';
position: absolute;
inset: 0;
background: var(--velo);
mix-blend-mode: lighten;
opacity: 0;
transition: opacity 300ms ease;
}
.pillola__nome {
position: absolute;
z-index: 1;
top: 50%;
left: 50%;
font-family: 'Mona Sans Variable', system-ui, sans-serif;
/* un sesto del titolo, ma mai sotto gli 11 pixel: sul telefono resta leggibile */
font-size: max(11px, 0.16em);
font-weight: 620;
letter-spacing: 0.01em;
white-space: nowrap;
opacity: 0;
transform: translate(-50%, -50%) scale(0.92);
transition: opacity 300ms ease, transform 400ms var(--scorre);
}
.pillola:focus-visible {
outline: 2px solid var(--inchiostro);
outline-offset: 3px;
}
.pillola:active {
transform: scale(0.96);
}
/* al passaggio: più larga, schiarita, col suo nome (la transizione non aspetta più il ritardo d’apertura) */
.pillola--sopra {
--largo: 2.3em;
transition-delay: 0ms;
}
.pillola--sopra::after {
opacity: 0.84;
}
.pillola--sopra .pillola__nome {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
/* movimento ridotto: le pillole sono già aperte e cambiano sul posto */
.titolo--ridotto .pillola {
width: var(--largo);
margin: 0 0.12em;
transition: none;
}
.titolo--ridotto .pillola--sopra {
--largo: 1.45em;
}
.titolo--ridotto .pillola::after,
.titolo--ridotto .pillola__nome {
transition-duration: 120ms;
}/* Le pillole nel titolo
Un titolo grande con tre pillole fra le parole, ognuna con dentro una piccola scena disegnata. All’apertura
le pillole partono larghe zero e si aprono una dopo l’altra, e la riga si riapre per fargli posto. Passandoci
sopra una pillola si allarga ancora, spinge le parole, si schiarisce di un pastello e mostra dove porta.
Dentro, la scena scivola piano avanti e indietro. */
/* ---- valori da cambiare ---- */
const APRE = 400; /* millesimi prima che le pillole comincino ad aprirsi */
const RIDOTTO = matchMedia('(prefers-reduced-motion: reduce)').matches;
const titolo = document.querySelector('[data-titolo]');
const pillole = [...document.querySelectorAll('[data-pillola]')];
pillole.forEach((p, i) => p.style.setProperty('--i', i));
/* ---------------------------------------------------------------- l’apertura */
function apri() {
if (RIDOTTO) {
titolo.classList.add('titolo--ridotto');
return;
}
titolo.classList.add('titolo--aperto');
/* aperte le pillole, niente più ritardi: al passaggio devono rispondere subito */
setTimeout(() => pillole.forEach((p) => p.style.setProperty('--i', 0)), 1200 + pillole.length * 180);
/* la scena dentro ogni pillola scivola piano, ognuna col suo passo */
pillole.forEach((p, i) => {
gsap.fromTo(p.querySelector('svg'), { xPercent: -5 }, { xPercent: 5, duration: 5 + i * 1.3, ease: 'sine.inOut', yoyo: true, repeat: -1 });
});
}
/* ---------------------------------------------------------------- il passaggio */
let sopra = null;
function accendi(p) {
if (p === sopra) return;
if (sopra) sopra.classList.remove('pillola--sopra');
if (p) p.classList.add('pillola--sopra');
sopra = p;
}
addEventListener('pointermove', (e) => {
if (e.pointerType !== 'mouse') return;
accendi(e.target instanceof Element ? e.target.closest('[data-pillola]') : null);
});
document.documentElement.addEventListener('mouseleave', () => accendi(null));
pillole.forEach((p) => {
p.addEventListener('focus', () => accendi(p));
p.addEventListener('blur', () => { if (sopra === p) accendi(null); });
/* col dito: si accende al tocco e resta accesa il tempo di allargarsi, poi torna com’era */
p.addEventListener('pointerdown', (e) => { if (e.pointerType !== 'mouse') accendi(p); });
p.addEventListener('pointerup', (e) => { if (e.pointerType !== 'mouse') setTimeout(() => { if (sopra === p) accendi(null); }, 1200); });
/* nella demo i link non portano da nessuna parte */
p.addEventListener('click', (e) => e.preventDefault());
});
/* ---------------------------------------------------------------- l’avvio: prima il carattere, poi le pillole */
const pronto = document.fonts && document.fonts.load ? document.fonts.load('400 1em "Instrument Serif"') : Promise.resolve();
pronto.catch(() => {}).then(() => setTimeout(apri, RIDOTTO ? 0 : APRE));Each pill is an inline-block link inside the headline, 0.8em tall with a full border-radius: it is measured on the headline’s size, so it follows the text at any size. It starts with width 0 and zero margins: the line is set as if it were not there.
Once the font has loaded, the headline gets a class and the pills go to width 1.45em with a 1.2-second transition, each 180 milliseconds after the other. Animating width means reflowing every frame, but these are three elements in one line: the text reflowing is exactly what makes the effect.
On mouse over the width goes to 2.3em, an ::after with a pastel in mix-blend-mode: lighten, at 84%, brightens the scene while letting it show through, and the name, in another typeface, rises from scale 0.92. A single pointermove listener decides what is under the pointer; keyboard focus does the same.
The staggered delay is only for the opening: once it is over, the script resets each pill’s --i variable, so the response to the mouse is immediate. The scene inside drifts with a 5-second yoyo tween, slightly longer for each pill.
| Name | File | Default | What it changes |
|---|---|---|---|
--largo | style.css | 1.45em, 2.3em | The pill’s width when open and on mouse over, in the headline’s em. |
--velo | index.html | per pillola | The pastel that lightens the scene on mouse over. |
APRE | script.js | 400 | The milliseconds before the pills start opening. |
For the headline of a home page or a portfolio that wants to lead somewhere at once: the pills are links, and they can hold photos, videos or drawings.
The pills are measured on the headline, so they shrink with it; with a finger they light up on touch and stay lit for just over a second, long enough to widen, then return as they were.
The pills are already open and do not widen: on mouse over only the veil and the name change, with a short fade, and the scene inside stays still.
GSAP 3.15.0, Instrument Serif 5.3.0, Mona Sans (font variabile) 5.3.0.
Weight: 9.5 KB of our own files.
Tested on 27 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.
You’re signed in as .
Your plan is free. When lifetime access arrives, it will switch on for this same account.