
Lettere in piedi con l'ombra lunga
Testo, Scroll, 3D
NuovaGratis
Scorrendo, una parola enorme si ripete a scalini e perde l’ultima lettera: la I si inclina, cade sotto la parola e, toccando terra, diventa il bottone per scrivere.
Una cartella che funziona da sola: index.html, style.css, script.js e le librerie da CDN.
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>La lettera che cade</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-variable/big-shoulders@5.3.0/index.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<section class="scena" data-scena>
<h1 class="nascosto">Scrivi</h1>
<p class="cenno" data-cenno>Scorri piano: la parola si ripete a scalini e perde l’ultima lettera.</p>
<!-- sei copie della stessa parola, una sopra l'altra: l'ultima è davanti, e la sua I è un link -->
<div class="eco" data-eco>
<div class="eco__misura" aria-hidden="true" data-misura><span class="l">S</span><span class="l">C</span><span class="l">R</span><span class="l">I</span><span class="l">V</span><span class="l">I</span></div>
<div class="copia" data-copia>
<span class="copia__lastra"></span>
<span class="copia__parola" aria-hidden="true"><span class="l">S</span><span class="l">C</span><span class="l">R</span><span class="l">I</span><span class="l">V</span><span class="l">I</span></span>
</div>
<div class="copia" data-copia>
<span class="copia__lastra"></span>
<span class="copia__parola" aria-hidden="true"><span class="l">S</span><span class="l">C</span><span class="l">R</span><span class="l">I</span><span class="l">V</span><span class="l">I</span></span>
</div>
<div class="copia" data-copia>
<span class="copia__lastra"></span>
<span class="copia__parola" aria-hidden="true"><span class="l">S</span><span class="l">C</span><span class="l">R</span><span class="l">I</span><span class="l">V</span><span class="l">I</span></span>
</div>
<div class="copia" data-copia>
<span class="copia__lastra"></span>
<span class="copia__parola" aria-hidden="true"><span class="l">S</span><span class="l">C</span><span class="l">R</span><span class="l">I</span><span class="l">V</span><span class="l">I</span></span>
</div>
<div class="copia" data-copia>
<span class="copia__lastra"></span>
<span class="copia__parola" aria-hidden="true"><span class="l">S</span><span class="l">C</span><span class="l">R</span><span class="l">I</span><span class="l">V</span><span class="l">I</span></span>
</div>
<div class="copia copia--davanti" data-copia>
<span class="copia__lastra"></span>
<span class="copia__parola" aria-hidden="true"><span class="l">S</span><span class="l">C</span><span class="l">R</span><span class="l">I</span><span class="l">V</span><span class="l" data-i>I</span><span class="copia__base" data-base></span></span>
<a class="goccia" href="#scrivici" data-goccia aria-label="Scrivici" tabindex="-1"><span class="goccia__premi" data-premi><span class="goccia__corpo" data-corpo><span class="goccia__testo" data-etichetta></span></span></span></a>
</div>
</div>
</section>
<section class="coda" id="scrivici">
<h2 class="coda__titolo">Scrivici</h2>
<p class="coda__testo">Una riga basta. Qui va il modulo, o l’indirizzo a cui scrivere.</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="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrambleTextPlugin.min.js"></script>
<script src="script.js"></script>
</body>
</html>/* La lettera che cade */
:root {
--carta: #E4E6EA;
--inchiostro: #16181D;
--tenue: #565B65;
--blu: #2B45FF;
--blu-scuro: #1A31D6;
--su-blu: #F4F5F8;
--margine: clamp(18px, 4vw, 56px);
--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;
}
.nascosto {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
/* ---- la scena: resta ferma mentre la parola si ripete e la lettera cade ---- */
.scena {
position: relative;
height: var(--alto);
overflow: hidden;
}
.cenno {
position: absolute;
z-index: 2;
top: var(--margine);
left: var(--margine);
max-width: 30ch;
margin: 0;
color: var(--tenue);
font-size: clamp(0.95rem, 0.85rem + 0.35vw, 1.15rem);
font-weight: 520;
line-height: 1.35;
}
/* la parola: la misura dà il posto, le copie ci stanno sopra una dopo l'altra */
.eco {
position: absolute;
left: 50%;
top: 46%;
transform: translate(-50%, -50%);
font-family: 'Big Shoulders Variable', 'Mona Sans Variable', sans-serif;
font-size: 20vw;
font-weight: 900;
line-height: 0.82;
letter-spacing: -0.01em;
text-transform: uppercase;
white-space: nowrap;
}
.eco__misura {
visibility: hidden;
}
.l {
display: inline-block;
}
.copia {
position: absolute;
inset: 0;
isolation: isolate;
transform-origin: 50% 100%;
}
/* la lastra ha il colore della pagina: copre le copie dietro, così di loro resta solo la cima */
.copia__lastra {
position: absolute;
z-index: -1;
top: -9%;
right: -3%;
bottom: -8%;
left: -3%;
background: var(--carta);
}
.eco--pronta .copia--davanti [data-i] {
visibility: hidden;
}
.copia__base {
display: inline-block;
width: 0;
height: 0;
vertical-align: baseline;
}
/* ---- la I: una barra d'inchiostro al posto della lettera, che cadendo diventa un bottone ---- */
.goccia {
position: absolute;
display: block;
color: var(--su-blu);
text-decoration: none;
pointer-events: none;
outline: none;
-webkit-tap-highlight-color: transparent;
}
.goccia--giu {
pointer-events: auto;
cursor: pointer;
}
.goccia__premi {
display: block;
width: 100%;
height: 100%;
transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1);
}
.goccia--giu:active .goccia__premi {
transform: scale(0.96);
}
.goccia__corpo {
position: relative;
display: block;
width: 100%;
height: 100%;
background: var(--inchiostro);
/* quando tocca terra si schiaccia dal lato che guarda in basso */
transform-origin: 100% 50%;
transition: background-color 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.goccia--giu .goccia__corpo {
background: var(--blu);
}
@media (hover: hover) and (pointer: fine) {
.goccia--giu:hover .goccia__corpo {
background: var(--blu-scuro);
}
}
.goccia:focus-visible .goccia__corpo {
box-shadow: 0 0 0 3px var(--carta), 0 0 0 6px var(--blu);
}
/* il testo è girato al contrario della barra: quando la barra è a terra, si legge dritto */
.goccia__testo {
position: absolute;
left: 50%;
top: 50%;
font-family: 'Mona Sans Variable', system-ui, sans-serif;
font-size: var(--etichetta, 16px);
font-weight: 720;
letter-spacing: 0.04em;
line-height: 1;
white-space: nowrap;
transform: translate(-50%, -50%) rotate(-90deg);
}
/* ---- dove porta il bottone ---- */
.coda {
display: grid;
align-content: center;
justify-items: start;
gap: 1rem;
min-height: calc(var(--alto) * 0.72);
padding: var(--margine);
}
.coda__titolo {
margin: 0;
font-family: 'Big Shoulders Variable', sans-serif;
font-size: clamp(3.4rem, 13vw, 10rem);
font-weight: 900;
line-height: 0.82;
text-transform: uppercase;
}
.coda__testo {
max-width: 34ch;
margin: 0;
color: var(--tenue);
font-size: clamp(1rem, 0.9rem + 0.4vw, 1.25rem);
line-height: 1.45;
}/* La lettera che cade
Una parola enorme che, scorrendo, si ripete a scalini: sei copie una sopra l'altra, ognuna su una lastra
del colore della pagina, sempre più piccole e più in basso, così delle copie dietro resta solo la cima.
Poi l'ultima lettera, una I, si inclina e cade sotto la parola: toccando terra si schiaccia, diventa blu
e si scrive il suo nome. Era una lettera, adesso è un bottone. */
gsap.registerPlugin(ScrollTrigger, ScrambleTextPlugin);
/* ---- valori da cambiare ---- */
const ETICHETTA = 'SCRIVICI'; /* il nome del bottone: si scrive quando la I tocca terra */
const CORSA = 1.8; /* quanti schermi di scorrimento dura la scena */
const LETTERE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const DITO = 46; /* spessore minimo del bottone a terra, in pixel veri */
const RIDOTTO = matchMedia('(prefers-reduced-motion: reduce)').matches;
const MOUSE = matchMedia('(hover: hover) and (pointer: fine)').matches;
const scena = document.querySelector('[data-scena]');
const eco = document.querySelector('[data-eco]');
const misuraEl = document.querySelector('[data-misura]');
const copie = gsap.utils.toArray('[data-copia]');
const davanti = copie[copie.length - 1];
const lettera = davanti.querySelector('[data-i]');
const base = davanti.querySelector('[data-base]');
const goccia = document.querySelector('[data-goccia]');
const premi = goccia.querySelector('[data-premi]');
const corpo = goccia.querySelector('[data-corpo]');
const etichetta = goccia.querySelector('[data-etichetta]');
const cenno = document.querySelector('[data-cenno]');
const FRASI = [cenno.textContent, 'La I è caduta: adesso è un bottone.'];
/* ---------------------------------------------------------------- le misure */
/* tutto in coordinate della copia davanti, senza le sue trasformazioni: la I, il bottone, il punto d'arrivo */
const righello = document.createElement('canvas').getContext('2d');
const geo = { scala: 1 / 13, passoY: 2, sinistra: 0, cima: 0, largo: 10, alto: 10, spesso: 10, lungo: 10, dx: 0, dy: 0 };
function misura() {
const vw = scena.clientWidth;
const vh = scena.clientHeight;
const stretto = vw < 640;
/* ogni copia è un tredicesimo più piccola e scende del 2% (al telefono un dodicesimo e il 10%) */
geo.scala = stretto ? 1 / 12 : 1 / 13;
geo.passoY = stretto ? 10 : 2;
/* il corpo della parola: largo quanto lo schermo, ma mai più alto di così */
eco.style.fontSize = '100px';
const larga = misuraEl.offsetWidth;
righello.font = '900 100px "Big Shoulders Variable"';
const capo = righello.measureText('I').actualBoundingBoxAscent / 100;
const f = Math.min((vw * (stretto ? 0.9 : 0.86) * 100) / larga, (vh * (stretto ? 0.24 : 0.44)) / capo);
eco.style.fontSize = `${f.toFixed(2)}px`;
/* il riquadro dell'inchiostro della I: da dove parte la barra */
righello.font = `900 ${f}px "Big Shoulders Variable"`;
const m = righello.measureText('I');
const linea = base.offsetTop;
geo.sinistra = lettera.offsetLeft - m.actualBoundingBoxLeft;
geo.largo = m.actualBoundingBoxLeft + m.actualBoundingBoxRight;
geo.alto = m.actualBoundingBoxAscent;
geo.cima = linea - geo.alto;
/* il bottone a terra: spesso almeno un dito, lungo abbastanza per il suo nome */
const piccola = 1 - (copie.length - 1) * geo.scala;
geo.spesso = Math.max(geo.largo, DITO / piccola);
const corpoTesto = geo.spesso * 0.34;
righello.font = `720 ${corpoTesto}px "Mona Sans Variable"`;
const nome = righello.measureText(ETICHETTA).width + ETICHETTA.length * corpoTesto * 0.04;
geo.lungo = Math.max(geo.alto, nome + geo.spesso * 1.3);
goccia.style.setProperty('--etichetta', `${corpoTesto.toFixed(1)}px`);
goccia.style.left = `${geo.sinistra}px`;
goccia.style.top = `${geo.cima}px`;
/* coricato, il bottone finisce allineato al bordo destro della parola, poco sotto la riga */
const destra = geo.sinistra + geo.largo;
const arrivoX = destra - geo.lungo / 2;
const arrivoY = linea + geo.alto * 0.22 + geo.spesso / 2;
geo.dx = arrivoX - (geo.sinistra + geo.spesso / 2);
geo.dy = arrivoY - (geo.cima + geo.lungo / 2);
}
/* ---------------------------------------------------------------- l'atterraggio */
let giu = false;
let frase = 0;
function cambiaCenno(i, anima) {
if (frase === i) return;
frase = i;
if (!anima) { cenno.textContent = FRASI[i]; return; }
gsap.to(cenno, {
opacity: 0, y: -6, duration: 0.18, ease: 'power1.in', overwrite: true,
onComplete: () => {
cenno.textContent = FRASI[frase];
gsap.fromTo(cenno, { opacity: 0, y: 6 }, { opacity: 1, y: 0, duration: 0.4, ease: 'power2.out' });
},
});
}
function atterra(anima) {
giu = true;
goccia.classList.add('goccia--giu');
goccia.tabIndex = 0;
cambiaCenno(1, anima);
if (!anima) {
etichetta.textContent = ETICHETTA;
return;
}
/* tocca terra: si schiaccia, rimbalza appena e scrive il suo nome (il blu lo mette il CSS) */
gsap.fromTo(corpo, { scaleX: 0.72, scaleY: 1.12 }, { scaleX: 1, scaleY: 1, duration: 0.9, ease: 'elastic.out(1, 0.38)', overwrite: 'auto' });
gsap.to(etichetta, { duration: 0.9, scrambleText: { text: ETICHETTA, chars: LETTERE, speed: 0.6, revealDelay: 0.2 }, ease: 'none', overwrite: true });
}
function solleva() {
giu = false;
goccia.classList.remove('goccia--giu');
goccia.tabIndex = -1;
if (document.activeElement === goccia) goccia.blur();
cambiaCenno(0, true);
gsap.to(corpo, { scaleX: 1, scaleY: 1, duration: 0.25, overwrite: 'auto' });
gsap.to(etichetta, { duration: 0.35, scrambleText: { text: '', chars: LETTERE, speed: 1 }, ease: 'none', overwrite: true });
}
/* ---------------------------------------------------------------- la scena */
const ATTERRA = 2.15;
function montaScena() {
/* l'eco e poi la caduta, in un'unica linea del tempo legata allo scorrimento */
const tl = gsap.timeline({ paused: true, defaults: { ease: 'none' } });
tl.fromTo(copie, { scale: 1, yPercent: 0 }, { scale: (i) => 1 - i * geo.scala, yPercent: (i) => i * geo.passoY, duration: 1 }, 0)
/* la I si inclina sempre più in fretta, scivola un poco a sinistra e cade: arriva a terra alla massima velocità */
.fromTo(goccia, { rotation: 0 }, { rotation: 90, duration: 1, ease: 'power2.in' }, 1.15)
.fromTo(goccia, { x: 0 }, { x: () => geo.dx, duration: 1, ease: 'sine.in' }, 1.15)
.fromTo(goccia, { y: 0 }, { y: () => geo.dy, duration: 1, ease: 'power3.in' }, 1.15)
.fromTo(goccia, { width: () => geo.largo, height: () => geo.alto }, { width: () => geo.spesso, height: () => geo.lungo, duration: 1, ease: 'power1.in' }, 1.15)
.to({}, { duration: 0.3 });
tl.eventCallback('onUpdate', () => {
const t = tl.time();
if (!giu && t >= ATTERRA - 0.01) atterra(true);
else if (giu && t < ATTERRA - 0.07) solleva();
});
ScrollTrigger.addEventListener('refreshInit', misura);
ScrollTrigger.create({
trigger: scena,
start: 'top top',
end: () => `+=${Math.round(innerHeight * CORSA)}`,
pin: true,
scrub: 0.8,
animation: tl,
invalidateOnRefresh: true,
});
}
/* la parola entra lettera per lettera, e la I si alza per ultima */
function ingresso() {
if (scrollY > 8) return;
const t = gsap.timeline({ delay: 0.15 });
t.from(davanti.querySelectorAll('.copia__parola .l:not([data-i])'), { yPercent: 38, opacity: 0, duration: 1.1, ease: 'expo.out', stagger: 0.06 })
.from(premi, { scaleY: 0, transformOrigin: '50% 100%', duration: 0.9, ease: 'expo.out', clearProps: 'transform' }, 0.3)
.from(cenno, { opacity: 0, y: 8, duration: 0.8, ease: 'power2.out' }, 0.45);
}
/* ---------------------------------------------------------------- i gesti */
if (MOUSE) {
goccia.addEventListener('pointerenter', () => {
if (!giu || RIDOTTO) return;
gsap.to(etichetta, { duration: 0.4, scrambleText: { text: ETICHETTA, chars: LETTERE, speed: 1 }, ease: 'none', overwrite: true });
});
}
goccia.addEventListener('click', (e) => {
e.preventDefault();
if (!giu) return;
const meta = document.querySelector(goccia.getAttribute('href'));
window.scrollTo({ top: meta.getBoundingClientRect().top + scrollY, behavior: RIDOTTO ? 'auto' : 'smooth' });
meta.setAttribute('tabindex', '-1');
meta.focus({ preventScroll: true });
});
/* ---------------------------------------------------------------- l'avvio: prima i caratteri, poi le misure */
function avvia() {
misura();
eco.classList.add('eco--pronta');
if (RIDOTTO) {
/* movimento ridotto: la scena è già alla fine, la I già a terra col suo nome */
const fine = () => {
misura();
gsap.set(copie, { scale: (i) => 1 - i * geo.scala, yPercent: (i) => i * geo.passoY });
gsap.set(goccia, { rotation: 90, x: geo.dx, y: geo.dy, width: geo.spesso, height: geo.lungo });
};
fine();
atterra(false);
let attesa = null;
addEventListener('resize', () => { clearTimeout(attesa); attesa = setTimeout(fine, 150); });
return;
}
montaScena();
ingresso();
}
Promise.all([
document.fonts.load('900 100px "Big Shoulders Variable"', 'SCRIVI'),
document.fonts.load('720 20px "Mona Sans Variable"', ETICHETTA),
]).catch(() => {}).then(avvia);Le copie sono sei div uno sull’altro con la stessa parola, e ognuno ha dietro una lastra del colore della pagina, un poco più grande della parola. La linea del tempo porta la copia i a scala 1 − i/13 e la fa scendere del 2i% (al telefono 1 − i/12 e 10i%), con l’origine in basso: le copie dietro restano più grandi, e di loro spunta solo la cima.
Nella copia davanti la I è nascosta e al suo posto c’è un link: una barra d’inchiostro messa esattamente sull’inchiostro della lettera. Il riquadro viene da measureText su un canvas (actualBoundingBoxLeft, Right e Ascent) e la linea di base da uno span alto zero allineato alla base.
Nella stessa linea del tempo, dopo una pausa, la barra ruota di 90° con power2.in, scende con power3.in e scivola a sinistra con sine.in: accelera come una cosa che si inclina, e arriva a terra alla massima velocità. Intanto si allarga fino a uno spessore di almeno 46 pixel veri, perché a terra dovrà premerla un dito.
Quando la linea del tempo passa il punto d’arrivo parte un’animazione a tempo, non legata allo scorrimento: la barra si schiaccia (0,72 per 1,12) e torna com’era con elastic.out, diventa blu e ScrambleText ci scrive il nome. Tornando indietro succede il contrario, e il link esce dal giro del Tab.
| Nome | File | Di partenza | Cosa cambia |
|---|---|---|---|
ETICHETTA | script.js | SCRIVICI | Il nome del bottone. La parola sta nell’HTML, lettera per lettera: cambiandola, l’ultima deve restare una lettera a bastone, come la I. |
CORSA | script.js | 1.8 | Quanti schermi di scorrimento dura la scena. |
DITO | script.js | 46 | Lo spessore minimo del bottone a terra, in pixel veri. |
--blu | style.css | #2B45FF | Il colore che prende la I quando diventa un bottone. |
Per chiudere una pagina: la parola del contatto che diventa il bottone del contatto. Funziona con ogni parola che finisce con una lettera a bastone.
Le copie scendono di più, il 10% a copia, per riempire uno schermo alto, e la barra a terra si allarga fino a uno spessore comodo per il pollice.
Niente scena ferma e niente caduta: la parola è già a scalini e la I è già a terra, blu, col suo nome.
GSAP 3.15.0, Big Shoulders (font variabile) 5.3.0, Mona Sans (font variabile) 5.3.0.
Peso: 16 KB dei nostri file.
Provata il 27 settembre 2026
Puoi usarla nei tuoi progetti e in quelli dei tuoi clienti. Non puoi rivenderla né ridistribuirla così com’è. GSAP resta sotto la sua licenza Standard. Licenza completa.
Con l’email e la password del tuo account.
Hai fatto l’accesso come .
Il tuo piano è gratis. Quando arriverà, l’accesso a vita si attiverà su questo stesso account.