
Nastro che accelera con lo scorrimento
Scroll, Testo, Hover
A vita
La sigla «Q.» si apre come una fisarmonica nella parola intera: lo spazio si allarga spingendo il punto, e le lettere di mezzo salgono da sotto la linea in ordine sparso.
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>Il marchio a fisarmonica</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>
<header class="testa">
<!-- il marchio piccolo della testata: la sigla si apre nella parola intera -->
<button class="marchio marchio--piccolo" type="button" data-marchio="Quasi.">Quasi.</button>
<p class="testa__nota">Studio di grafica</p>
</header>
<main class="eroe">
<!-- lo stesso marchio alla misura di un titolo -->
<button class="marchio marchio--grande" type="button" data-marchio="Quasi." data-autoclic>Quasi.</button>
<p class="eroe__nota">Passa sopra il marchio, o toccalo.</p>
</main>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="script.js"></script>
</body>
</html>/* Il marchio a fisarmonica */
:root {
--fondo: #2340C8;
--testo: #F7F5EF;
--margine: clamp(18px, 4vw, 56px);
--schermo: 100vh;
}
@supports (height: 100svh) {
:root { --schermo: 100svh; }
}
* { box-sizing: border-box; }
html,
body {
margin: 0;
}
body {
font-family: 'Mona Sans Variable', system-ui, sans-serif;
color: var(--testo);
background: var(--fondo);
}
/* ------------------------------------------------------------------ il marchio */
.marchio {
display: inline-flex;
padding: 0;
border: 0;
background: none;
color: inherit;
font: inherit;
font-weight: 900;
font-stretch: 125%;
line-height: 1;
letter-spacing: -0.03em;
text-transform: uppercase;
white-space: nowrap;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.marchio:focus-visible {
outline: 2px solid var(--testo);
outline-offset: 0.12em;
}
/* la parte che si apre: la sua larghezza va da zero alla parola intera */
.marchio__mezzo {
display: inline-flex;
overflow: clip;
}
/* ogni lettera ha la sua maschera, alta una riga: sale da sotto la linea.
Taglia solo in verticale: con la spaziatura stretta il fianco della lettera esce dalla scatola */
.marchio__lettera {
display: inline-block;
flex: none;
overflow-x: visible;
overflow-y: clip;
}
.marchio__lettera > span {
display: inline-block;
}
.marchio--piccolo {
font-size: 28px;
}
.marchio--grande {
font-size: clamp(4rem, 1rem + 15vw, 17rem);
}
/* ------------------------------------------------------------------ la pagina */
.testa {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 22px var(--margine);
}
.testa__nota {
margin: 0;
font-size: 0.95rem;
font-weight: 560;
}
.eroe {
display: grid;
place-content: center;
justify-items: center;
gap: clamp(20px, 3vw, 40px);
min-height: var(--schermo);
padding: 96px var(--margine) var(--margine);
text-align: center;
}
.eroe__nota {
margin: 0;
font-size: clamp(1rem, 0.9rem + 0.4vw, 1.3rem);
font-weight: 500;
opacity: 0.85;
}
@media (max-width: 560px) {
.marchio--piccolo {
font-size: 22px;
}
.testa__nota {
font-size: 0.85rem;
}
}/* Il marchio a fisarmonica
A riposo il marchio è solo la sigla: la prima lettera e il punto. Passandoci sopra si apre come
una fisarmonica: lo spazio fra le due si allarga fino alla parola intera, spingendo il punto a
destra, e intanto le lettere di mezzo salgono da sotto la linea, ognuna nella sua maschera, in
un ordine diverso ogni volta. Uscendo le lettere ricadono e lo spazio si richiude. Ogni gesto
riparte da dove è arrivato il precedente, anche a metà. */
/* ---- valori da cambiare ---- */
const APRE = 0.7; /* secondi perché lo spazio si allarghi o si richiuda */
const SALE = 0.6; /* secondi di ogni lettera */
const SFASA_SU = 0.06; /* secondi fra una lettera e l'altra quando salgono */
const SFASA_GIU = 0.08; /* secondi fra una lettera e l'altra quando ricadono */
const RITARDO = 0.35; /* le lettere partono quando lo spazio è già un po' aperto */
const CURVA = 'power4.inOut';
const RIDOTTO = matchMedia('(prefers-reduced-motion: reduce)').matches;
/* da "Quasi." a: sigla, lettere di mezzo nelle loro maschere, punto */
function costruisci(el) {
const parola = el.dataset.marchio;
const mezzo = [...parola.slice(1, -1)];
el.setAttribute('aria-label', parola.replace(/[.'’*]$/, ''));
el.innerHTML = `<span aria-hidden="true">${parola[0]}</span>`
+ `<span class="marchio__mezzo" aria-hidden="true">${mezzo.map((c) => `<span class="marchio__lettera"><span>${c}</span></span>`).join('')}</span>`
+ `<span aria-hidden="true">${parola.slice(-1)}</span>`;
const m = {
el,
mezzo: el.querySelector('.marchio__mezzo'),
maschere: [...el.querySelectorAll('.marchio__lettera')],
lettere: [...el.querySelectorAll('.marchio__lettera > span')],
aperto: false,
tl: null,
};
gsap.set(m.mezzo, { width: 0 });
gsap.set(m.lettere, RIDOTTO ? { autoAlpha: 0 } : { yPercent: 110 });
return m;
}
/* la larghezza della parola aperta: la somma delle maschere, che non dipende dallo spazio attuale */
const intera = (m) => m.maschere.reduce((s, l) => s + l.getBoundingClientRect().width, 0);
function apri(m) {
if (m.aperto) return;
m.aperto = true;
if (m.tl) m.tl.kill();
if (RIDOTTO) {
gsap.set(m.mezzo, { width: 'auto' });
m.tl = gsap.to(m.lettere, { autoAlpha: 1, duration: 0.2, ease: 'none' });
return;
}
const larga = intera(m);
const gia = Math.min(1, m.mezzo.getBoundingClientRect().width / larga);
const giu = m.lettere.filter((l) => gsap.getProperty(l, 'yPercent') > 0.5);
m.tl = gsap.timeline({ onComplete: () => gsap.set(m.mezzo, { width: 'auto' }) });
/* se lo spazio è già aperto (si era a metà della chiusura) salgono solo le lettere */
if (gia < 0.99) m.tl.to(m.mezzo, { width: larga, duration: APRE * Math.max(0.4, 1 - gia), ease: CURVA }, 0);
m.tl.to(gsap.utils.shuffle(giu), { yPercent: 0, duration: SALE, ease: CURVA, stagger: SFASA_SU }, gia < 0.99 ? RITARDO * (1 - gia) : 0);
}
function chiudi(m) {
if (!m.aperto) return;
m.aperto = false;
if (m.tl) m.tl.kill();
if (RIDOTTO) {
m.tl = gsap.to(m.lettere, { autoAlpha: 0, duration: 0.2, ease: 'none', onComplete: () => gsap.set(m.mezzo, { width: 0 }) });
return;
}
/* da "auto" a pixel, così la larghezza si può animare */
gsap.set(m.mezzo, { width: m.mezzo.getBoundingClientRect().width });
const su = m.lettere.filter((l) => gsap.getProperty(l, 'yPercent') < 109.5);
m.tl = gsap.timeline();
m.tl.to(gsap.utils.shuffle(su), { yPercent: 110, duration: SALE, ease: CURVA, stagger: SFASA_GIU }, 0);
m.tl.to(m.mezzo, { width: 0, duration: APRE, ease: CURVA }, su.length ? '>-0.3' : 0);
}
const marchi = [...document.querySelectorAll('[data-marchio]')].map(costruisci);
let ultimoDito = 'mouse';
marchi.forEach((m) => {
m.el.addEventListener('pointerenter', (e) => { if (e.pointerType === 'mouse') apri(m); });
m.el.addEventListener('pointerleave', (e) => { if (e.pointerType === 'mouse') chiudi(m); });
m.el.addEventListener('pointerdown', (e) => { ultimoDito = e.pointerType; });
m.el.addEventListener('focus', () => { if (m.el.matches(':focus-visible')) apri(m); });
m.el.addEventListener('blur', () => chiudi(m));
m.el.addEventListener('click', (e) => {
/* col mouse comanda il passaggio; il dito, la tastiera e il pilota automatico usano il clic */
if (e.detail > 0 && ultimoDito === 'mouse') return;
if (m.aperto) chiudi(m); else apri(m);
});
});
/* la prima volta che si vedono, i marchi si aprono e si richiudono da soli, per mostrare il gesto */
if (!RIDOTTO) {
const visti = new IntersectionObserver((voci) => {
voci.forEach((v) => {
if (!v.isIntersecting) return;
visti.unobserve(v.target);
const m = marchi.find((x) => x.el === v.target);
const dopo = m.el.classList.contains('marchio--grande') ? 0.7 : 1.1;
gsap.delayedCall(dopo, () => apri(m));
gsap.delayedCall(dopo + 2.6, () => { if (!m.el.matches(':hover, :focus-visible')) chiudi(m); });
});
}, { threshold: 0.6 });
document.fonts.ready.then(() => marchi.forEach((m) => visti.observe(m.el)));
}Lo script divide la parola in sigla, lettere di mezzo e punto. Le lettere di mezzo stanno in un contenitore flex con overflow: clip, largo zero a riposo: il punto è subito dopo la sigla, e segue da solo lo spazio che si allarga.
La larghezza del contenitore va da zero alla somma delle maschere delle lettere in 0,7 secondi, con power4.inOut. Alla fine torna ad «auto», così se la finestra cambia misura la parola aperta si adatta; prima di chiudere si rimette in pixel per poterla animare.
Ogni lettera sta in una maschera che taglia solo in verticale e sale da yPercent 110 a 0 in 0,6 secondi, 60 millesimi dopo l’altra, a partire da 0,35 secondi. L’ordine è rimescolato ogni volta con gsap.utils.shuffle: il marchio non si apre mai due volte uguale.
Ogni gesto uccide la sequenza in corso e ne costruisce una nuova da dove si trova: se lo spazio è già aperto salgono solo le lettere, senza aspettare; se una lettera è già su, non riparte. Col mouse comanda il passaggio, col dito e con la tastiera il clic.
| Nome | File | Di partenza | Cosa cambia |
|---|---|---|---|
APRE | script.js | 0.7 | Secondi perché lo spazio si allarghi o si richiuda. |
SALE | script.js | 0.6 | Secondi di ogni lettera che sale o ricade. |
SFASA_SU | script.js | 0.06 | Secondi fra una lettera e l’altra quando salgono (quando ricadono è SFASA_GIU, 0.08). |
RITARDO | script.js | 0.35 | Dopo quanti secondi di apertura partono le lettere. |
Per il marchio in testata di un sito che ha una sigla forte: a riposo occupa poco, al passaggio si presenta per intero. Funziona anche grande, come titolo della prima schermata.
Senza passaggio del mouse il marchio si apre e si chiude al tocco. La prima volta che si vede si apre da solo e dopo un paio di secondi si richiude, per mostrare che c’è.
Niente lettere che salgono e niente fisarmonica: la parola compare e scompare con una dissolvenza di due decimi di secondo, e non si apre da sola.
GSAP 3.15.0, Mona Sans (font variabile) 5.3.0.
Peso: 8,2 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.