
Nastro che accelera con lo scorrimento
Scroll, Testo, Hover
A vita
Passando sulle lettere ognuna fa il salto dei cartoni animati: si schiaccia, sale, prende un colore e ricade con un rimbalzo. Le vicine fanno l’onda.
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>Lettere che saltano</title>
<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>
<main class="palco">
<p class="sopra">Passa sulle lettere col mouse, o strisciaci sopra col dito</p>
<h1 class="titolo" data-salta>
<span class="riga">Lettere</span>
<span class="riga">che saltano!</span>
</h1>
<p class="sotto">Ogni lettera si schiaccia, salta, prende un colore e ricade con un rimbalzo. Le vicine fanno l’onda.</p>
</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/SplitText.min.js"></script>
<script src="script.js"></script>
</body>
</html>/* Lettere che saltano */
:root {
--fondo: #07201f;
--lettere: #f6f7f3;
--tenue: #86a8a2;
}
* { box-sizing: border-box; }
html,
body {
margin: 0;
height: 100%;
}
body {
font-family: 'Mona Sans Variable', system-ui, sans-serif;
background: var(--fondo);
color: var(--lettere);
overflow: hidden;
}
.palco {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 100vh;
min-height: 100dvh;
padding: clamp(20px, 6vw, 88px);
}
.sopra {
margin: 0 0 clamp(14px, 3vh, 28px);
font-size: 14px;
font-weight: 620;
color: var(--tenue);
}
.titolo {
margin: 0;
font-size: clamp(54px, 12.5vw, 210px);
font-weight: 900;
font-stretch: 112%;
line-height: 0.92;
letter-spacing: -0.04em;
/* il dito può strisciare di lato sul titolo: la pagina scorre solo in verticale */
touch-action: pan-y;
user-select: none;
-webkit-user-select: none;
}
.riga {
display: block;
}
/* le lettere saltano fuori dalla riga: niente maschere */
.lettera {
display: inline-block;
transform-origin: 50% 100%;
will-change: transform;
}
.sotto {
max-width: 40ch;
margin: clamp(18px, 4vh, 36px) 0 0;
font-size: clamp(15px, 1.2vw, 18px);
line-height: 1.45;
color: #c9dcd8;
}/* Lettere che saltano
Ogni lettera del titolo, quando il puntatore (o il dito) ci passa sopra, fa il salto dei cartoni
animati: si schiaccia e si allarga, sale, si inclina un po' a caso, prende un colore; poi ricade
con una curva elastica e torna al suo colore. Le due lettere vicine per lato fanno l'onda, più
piccola e un attimo dopo. Col dito si striscia di lato sul titolo: la lettera sotto il dito salta. */
gsap.registerPlugin(SplitText);
/* ---- valori da cambiare ---- */
const COLORI = ['#ff8a3d', '#ffb27a', '#86cfc4', '#3aa39b', '#ffd27a']; /* il salto ne pesca uno */
const SALTO = -20; /* di quanto sale la lettera, in percentuale della sua altezza */
const INCLINA = 12; /* rotazione massima, in gradi */
const RIMBALZO = 'elastic.out(1, 0.32)';
const ONDA = [-8, -3]; /* quanto salgono la prima e la seconda vicina */
const RICHIAMO = 5; /* secondi fra un'onda automatica e l'altra, quando nessuno tocca il titolo */
const RIDOTTO = matchMedia('(prefers-reduced-motion: reduce)').matches;
const titolo = document.querySelector('[data-salta]');
const COLORE = getComputedStyle(document.documentElement).getPropertyValue('--lettere').trim() || '#f6f7f3';
function salta(l) {
gsap.timeline({ defaults: { overwrite: 'auto' } })
.to(l, {
yPercent: SALTO, scaleX: 1.12, scaleY: 0.86,
rotation: gsap.utils.random(-INCLINA, INCLINA), color: gsap.utils.random(COLORI),
duration: 0.16, ease: 'power2.out',
})
.to(l, { yPercent: 0, scaleX: 1, scaleY: 1, rotation: 0, duration: 1, ease: RIMBALZO })
.to(l, { color: COLORE, duration: 0.7, ease: 'power1.out' }, '-=0.85');
}
function onda(l, forza, ritardo) {
gsap.timeline({ delay: ritardo, defaults: { overwrite: 'auto' } })
.to(l, { yPercent: forza, duration: 0.16, ease: 'power2.out' })
.to(l, { yPercent: 0, duration: 0.9, ease: 'elastic.out(1, 0.4)' });
}
document.fonts.ready.then(() => {
if (RIDOTTO) return;
/* ogni parola in un blocco a sé, così l'onda resta dentro la parola */
const split = SplitText.create(titolo, { type: 'words,chars', charsClass: 'lettera' });
const parole = split.words.map((w) => [...w.querySelectorAll('.lettera')]);
let ultima = null;
let tocco = -Infinity;
function colpisci(l, dalPuntatore = true) {
if (!l || l === ultima) return;
ultima = l;
if (dalPuntatore) tocco = performance.now();
salta(l);
const parola = parole.find((p) => p.includes(l));
const i = parola.indexOf(l);
[[i - 1, 0], [i + 1, 0], [i - 2, 1], [i + 2, 1]].forEach(([j, k]) => {
if (parola[j]) onda(parola[j], ONDA[k], (k + 1) * 0.05);
});
}
/* col mouse: basta entrare nella lettera */
parole.flat().forEach((l) => {
l.addEventListener('pointerenter', (e) => { if (e.pointerType === 'mouse') colpisci(l); });
l.addEventListener('pointerleave', () => { if (ultima === l) ultima = null; });
});
/* col dito: il tocco resta "catturato" dal primo elemento, quindi si cerca la lettera sotto il dito */
titolo.addEventListener('pointerdown', (e) => {
if (e.pointerType === 'mouse') return;
ultima = null;
colpisci(e.target.closest('.lettera'));
});
titolo.addEventListener('pointermove', (e) => {
if (e.pointerType === 'mouse') return;
const sotto = document.elementFromPoint(e.clientX, e.clientY);
colpisci(sotto && sotto.closest ? sotto.closest('.lettera') : null);
});
/* ingresso: le lettere cadono dall'alto una dopo l'altra, già con il rimbalzo */
gsap.from(split.chars, { yPercent: -140, rotation: () => gsap.utils.random(-20, 20), autoAlpha: 0, duration: 1.1, ease: 'bounce.out', stagger: 0.035 });
/* richiamo: se nessuno tocca il titolo, ogni tanto un'onda lo attraversa da sola e mostra il gioco */
const tutte = split.chars;
function giro() {
if (performance.now() - tocco > RICHIAMO * 600) {
tutte.forEach((l, i) => gsap.delayedCall(i * 0.06, () => { ultima = null; colpisci(l, false); }));
}
gsap.delayedCall(RICHIAMO, giro);
}
gsap.delayedCall(2.2, giro);
});SplitText divide il titolo in parole e lettere; ogni lettera è un inline-block con l’origine in basso al centro, così schiacciarla la tiene appoggiata alla riga.
Prima un colpo corto (0,16 s): scaleX 1,12 e scaleY 0,86 mentre sale del 20% della sua altezza. È il principio dei cartoni: il peso si vede nel momento in cui la lettera si deforma.
Poi un secondo intero con elastic.out(1, 0.32): la lettera torna giù, supera la riga di poco e ondeggia. Il colore torna al suo posto in parallelo, un po’ più lento.
Le due vicine per lato salgono dell’8% e del 3% con 50 e 100 millesimi di ritardo, solo dentro la stessa parola. Col dito il tocco resta catturato dal primo elemento: a ogni pointermove si cerca la lettera sotto il dito con elementFromPoint.
| Nome | File | Di partenza | Cosa cambia |
|---|---|---|---|
COLORI | script.js | arancio, pesca, menta, verde acqua, giallo | I colori che il salto pesca a caso. |
SALTO | script.js | -20 | Di quanto sale la lettera, in percentuale della sua altezza. |
INCLINA | script.js | 12 | La rotazione massima in gradi, in un verso a caso. |
RIMBALZO | script.js | elastic.out(1, 0.32) | La ricaduta: abbassa il secondo numero per più oscillazioni. |
RICHIAMO | script.js | 5 | Ogni quanti secondi un'onda attraversa il titolo da sola, se nessuno lo tocca. Toglila se il titolo deve stare fermo. |
Per un titolo grande con carattere: un portfolio, un marchio giocoso, un’insegna. Non per i testi da leggere: un paragrafo che salta non si legge.
Si striscia col dito di lato sul titolo e la lettera sotto il dito salta; toccare una lettera la fa saltare. Il titolo lascia scorrere la pagina in verticale (touch-action: pan-y).
Le lettere stanno ferme.
GSAP 3.15.0, SplitText (GSAP) 3.15.0, Mona Sans (font variabile) 5.3.0.
Peso: 6,1 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.