
Parole che si fondono
Testo, Scroll, SVG
NuovaGratis
Un conteggio fino a cento, un binario che si allunga, poi un rettangolo che si scopre e diventa il marchio mentre le righe escono dall'inchiostro.
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 caricamento che diventa marchio</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>
<!-- i filtri "inchiostro" per le righe li crea lo script qui dentro -->
<svg class="filtri" aria-hidden="true" focusable="false"><defs></defs></svg>
<header class="angoli">
<button type="button" class="angolo angolo--griglia" aria-label="Griglia"><span></span><span></span><span></span><span></span></button>
<button type="button" class="angolo angolo--menu">Menu</button>
</header>
<main class="pagina">
<div class="sinistra">
<!-- il marchio: nasce rettangolo, poi il morph lo porta alla forma finale (data-forma) -->
<div class="marchio">
<svg viewBox="0 0 96 160" aria-hidden="true">
<path class="marchio__forma" d="M0 0 L96 0 L96 160 L0 160 Z"
data-forma="M48 4 C68 22 86 44 88 64 C78 68 72 80 78 92 C86 104 90 118 84 134 C76 150 62 158 44 158 C22 158 6 140 6 114 C6 88 22 66 34 48 C42 34 44 18 48 4 Z" />
</svg>
</div>
<p class="meta" data-inchiostro>Disegno<br>e sviluppo</p>
<p class="conta" aria-hidden="true"><span class="conta__riga"><span class="conta__cifre">0</span>%</span></p>
</div>
<span class="binario" aria-hidden="true"></span>
<div class="destra">
<p class="meta meta--su" data-inchiostro>Lavora da Brescia<br>con studi di tutta Europa</p>
<h1 class="nome" data-inchiostro>Anna Ferri</h1>
<p class="frase" data-inchiostro>Disegno prodotti digitali, interfacce e siti che si muovono.</p>
</div>
</main>
<button type="button" class="rivedi" hidden>Rivedi il caricamento</button>
<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/MorphSVGPlugin.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>/* Il caricamento che diventa marchio */
:root {
--carta: #f6f7f3; /* il fondo */
--inchiostro: #07201f; /* testo, marchio, binario */
--tenue: #6b7f7c;
--accento: #ff8a3d;
}
* { box-sizing: border-box; }
html,
body {
margin: 0;
min-height: 100%;
}
body {
font-family: 'Mona Sans Variable', system-ui, sans-serif;
color: var(--inchiostro);
background: var(--carta);
overflow: hidden;
}
.filtri {
position: absolute;
width: 0;
height: 0;
}
/* i due pulsanti d'angolo entrano da fuori quando il conteggio finisce */
.angoli {
position: fixed;
inset: 0 0 auto 0;
z-index: 5;
display: flex;
justify-content: space-between;
padding: 18px;
pointer-events: none;
}
.angolo {
pointer-events: auto;
border: 0;
font: inherit;
cursor: pointer;
visibility: hidden;
}
.angolo--griglia {
display: grid;
grid-template-columns: repeat(2, 7px);
gap: 4px;
place-content: center;
width: 36px;
height: 36px;
padding: 0;
border-radius: 8px;
background: #e6ebe8;
}
.angolo--griglia span {
width: 7px;
height: 7px;
border-radius: 1.5px;
background: var(--inchiostro);
}
.angolo--menu {
padding: 8px 16px;
border-radius: 999px;
background: var(--inchiostro);
color: var(--carta);
font-weight: 640;
font-size: 15px;
}
/* la pagina: colonna stretta, binario, colonna larga */
.pagina {
display: grid;
grid-template-columns: minmax(0, 3fr) 1px minmax(0, 7fr);
min-height: 100vh;
min-height: 100dvh;
padding: 0 clamp(18px, 5vw, 72px);
column-gap: clamp(18px, 3vw, 44px);
}
.sinistra,
.destra {
position: relative;
display: flex;
flex-direction: column;
padding: 18px 0 clamp(20px, 4vh, 44px);
}
.sinistra {
justify-content: space-between;
align-items: flex-start;
}
.destra {
justify-content: flex-end;
gap: clamp(14px, 2vh, 24px);
}
.binario {
width: 1px;
height: 0;
align-self: start;
background: var(--inchiostro);
}
.marchio {
width: clamp(54px, 8vw, 104px);
margin-top: clamp(46px, 7vh, 70px);
visibility: hidden;
}
.marchio svg {
display: block;
width: 100%;
height: auto;
overflow: visible;
}
.marchio__forma {
fill: var(--inchiostro);
}
.meta {
margin: 0;
font-size: clamp(13px, 1.05vw, 16px);
font-weight: 560;
line-height: 1.25;
}
.meta--su {
position: absolute;
top: 18px;
left: 0;
}
.nome {
margin: 0;
font-size: clamp(34px, 6.4vw, 104px);
font-weight: 700;
font-stretch: 92%;
line-height: 0.92;
letter-spacing: -0.045em;
}
.frase {
margin: 0;
max-width: 13ch;
font-size: clamp(40px, 8.2vw, 140px);
font-weight: 700;
font-stretch: 88%;
line-height: 0.9;
letter-spacing: -0.05em;
}
/* il conteggio sale da una maschera e se ne va dalla stessa */
.conta {
margin: 0;
overflow: clip;
font-size: clamp(44px, 7vw, 116px);
font-weight: 700;
font-stretch: 80%;
letter-spacing: -0.05em;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.conta__riga {
display: block;
}
.rivedi {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 5;
padding: 10px 16px;
border: 1px solid rgba(7, 32, 31, 0.2);
border-radius: 999px;
background: var(--carta);
color: var(--inchiostro);
font: 600 14px/1 'Mona Sans Variable', system-ui, sans-serif;
cursor: pointer;
transition: transform 0.16s ease-out, border-color 0.2s ease;
}
.rivedi:hover {
border-color: var(--accento);
}
.rivedi:active {
transform: scale(0.97);
}
/* sul telefono le colonne si stringono: il marchio resta a sinistra, il testo a destra */
@media (max-width: 700px) {
.pagina {
grid-template-columns: minmax(0, 1fr) 1px minmax(0, 3fr);
}
.meta--su {
top: 64px;
}
.frase {
max-width: 9ch;
}
/* il numero deve stare nella colonna stretta, il pulsante non deve coprire il testo */
.conta {
font-size: 8.4vw;
}
.rivedi {
left: 18px;
right: auto;
}
.destra {
padding-bottom: 76px;
}
}
/* movimento ridotto: tutto al suo posto dall'inizio */
.fermo .angolo,
.fermo .marchio {
visibility: visible;
}
.fermo .binario {
height: 100%;
}
.fermo .conta {
visibility: hidden;
}/* Il caricamento che diventa marchio
Un conteggio sale fino a cento mentre il binario si allunga dall'alto. A cento il numero se ne va
dalla sua maschera, i due pulsanti d'angolo entrano da fuori e nasce il marchio: prima un rettangolo
che si scopre dall'alto, poi il morph lo porta alla sua forma. Intanto le righe di testo escono
dall'inchiostro: una sfocatura forte tagliata da una soglia (un filtro SVG per riga) le fa comparire
come macchie che si stringono fino a diventare lettere. */
gsap.registerPlugin(MorphSVGPlugin, SplitText);
/* ---- valori da cambiare ---- */
const CONTEGGIO = 2.2; /* secondi per arrivare a cento */
const SFOCATURA = 0.32; /* quanto è sfocata una riga all'inizio, in frazione della sua altezza */
const SOGLIA = '0 0 0 22 -8'; /* ultima riga della matrice: più alto il primo numero, più netti i bordi delle macchie */
const SFASATURA = 0.09; /* secondi fra una riga e la successiva */
const FUORI = matchMedia('(max-width: 700px)').matches ? '25vw' : '6vw'; /* da quanto lontano arrivano i pulsanti */
const CURVA = 'power3.inOut';
const RIDOTTO = matchMedia('(prefers-reduced-motion: reduce)').matches;
const cifre = document.querySelector('.conta__cifre');
const riga = document.querySelector('.conta__riga');
const binario = document.querySelector('.binario');
const marchio = document.querySelector('.marchio');
const forma = document.querySelector('.marchio__forma');
const angoli = [...document.querySelectorAll('.angolo')];
const rivedi = document.querySelector('.rivedi');
const defs = document.querySelector('.filtri defs');
const RETTANGOLO = forma.getAttribute('d');
const FINALE = forma.dataset.forma;
const SVG = 'http://www.w3.org/2000/svg';
let righe = [];
let sfocature = [];
let tl = null;
/* ogni blocco di testo diviso in righe; ogni riga ha il suo filtro, così le sfocature vanno sfalsate */
function preparaRighe() {
document.querySelectorAll('[data-inchiostro]').forEach((el) => {
SplitText.create(el, { type: 'lines', linesClass: 'riga-inchiostro' }).lines.forEach((l) => righe.push(l));
});
sfocature = righe.map((l, i) => {
const f = document.createElementNS(SVG, 'filter');
f.id = `inchiostro-${i}`;
/* la zona del filtro deve contenere la sfocatura, altrimenti le macchie vengono tagliate */
f.setAttribute('x', '-25%');
f.setAttribute('y', '-90%');
f.setAttribute('width', '150%');
f.setAttribute('height', '280%');
f.setAttribute('color-interpolation-filters', 'sRGB');
const blur = document.createElementNS(SVG, 'feGaussianBlur');
blur.setAttribute('in', 'SourceGraphic');
blur.setAttribute('stdDeviation', '0');
const soglia = document.createElementNS(SVG, 'feColorMatrix');
soglia.setAttribute('type', 'matrix');
soglia.setAttribute('values', `1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 ${SOGLIA}`);
f.append(blur, soglia);
defs.appendChild(f);
return blur;
});
}
/* tutto torna com'era prima del caricamento */
function azzera() {
if (tl) tl.kill();
cifre.textContent = '0';
gsap.set(binario, { height: 0 });
gsap.set(riga, { yPercent: 100 });
gsap.set(angoli, { autoAlpha: 0 });
gsap.set(marchio, { autoAlpha: 0, clipPath: 'inset(0% 0% 100% 0%)' });
gsap.set(forma, { attr: { d: RETTANGOLO } });
gsap.set(righe, { opacity: 0, filter: 'none' });
rivedi.hidden = true;
rivedi.removeAttribute('data-autoclic');
}
function sequenza() {
azzera();
const stato = { n: 0 };
const fine = CONTEGGIO + 0.1;
tl = gsap.timeline();
tl.to(binario, { height: '100%', duration: CONTEGGIO + 0.5, ease: CURVA }, 0)
.to(riga, { yPercent: 0, duration: 0.7, ease: 'expo.out' }, 0.05)
.to(stato, { n: 100, duration: CONTEGGIO, ease: CURVA, onUpdate: () => { cifre.textContent = Math.round(stato.n); } }, 0)
.to(riga, { yPercent: -105, duration: 0.55, ease: 'expo.in' }, fine)
/* i pulsanti arrivano dagli angoli, in diagonale. immediateRender false: i valori di partenza
si applicano solo quando tocca a loro, altrimenti sul telefono si vedrebbero già dall'inizio */
.set(angoli, { autoAlpha: 1 }, fine + 0.15)
.fromTo(angoli[0], { x: `-${FUORI}`, y: `-${FUORI}` }, { x: 0, y: 0, duration: 1, ease: 'expo.out', immediateRender: false }, fine + 0.15)
.fromTo(angoli[1], { x: FUORI, y: `-${FUORI}` }, { x: 0, y: 0, duration: 1, ease: 'expo.out', immediateRender: false }, fine + 0.15)
/* il marchio: il rettangolo si scopre dall'alto, poi diventa la forma */
.set(marchio, { autoAlpha: 1 }, fine + 0.2)
.to(marchio, { clipPath: 'inset(0% 0% 0% 0%)', duration: 0.75, ease: 'expo.out' }, fine + 0.2)
.to(forma, { morphSVG: FINALE, duration: 1.2, ease: 'expo.inOut' }, fine + 0.8);
/* le righe escono dall'inchiostro: la sfocatura scende a zero, la soglia fa il resto */
righe.forEach((l, i) => {
const h = l.offsetHeight || 40;
const at = fine + 0.35 + i * SFASATURA;
tl.set(l, { opacity: 1, filter: `url(#inchiostro-${i})` }, at)
.fromTo(sfocature[i], { attr: { stdDeviation: (h * SFOCATURA).toFixed(1) } }, {
attr: { stdDeviation: 0 }, duration: 1.25, ease: 'power2.out',
/* a riposo il filtro si toglie: il testo torna nitido e il browser lavora meno */
onComplete: () => { l.style.filter = 'none'; },
}, at);
});
tl.add(() => {
rivedi.hidden = false;
gsap.fromTo(rivedi, { autoAlpha: 0, y: 12 }, { autoAlpha: 1, y: 0, duration: 0.5, ease: 'expo.out' });
/* l'anteprima automatica della libreria lo ripete da sola */
rivedi.setAttribute('data-autoclic', '');
}, '+=0.4');
}
function avvia() {
preparaRighe();
if (RIDOTTO) {
document.documentElement.classList.add('fermo');
forma.setAttribute('d', FINALE);
return;
}
rivedi.addEventListener('click', sequenza);
sequenza();
}
/* le righe si misurano col carattere vero: prima si aspetta che sia caricato */
document.fonts.ready.then(avvia);Un oggetto { n: 0 } va a 100 con la stessa curva power3.inOut che allunga il binario da height 0 a 100%: numero e riga arrivano insieme. Il numero sta in una riga con overflow clip, così entra ed esce dalla sua maschera con yPercent.
A conteggio finito i due pulsanti partono spostati di 6vw in diagonale fuori dallo schermo e tornano a zero con expo.out. È un dettaglio piccolo, ma dice che il sito è pronto.
Il path parte come rettangolo (M0 0 L96 0 L96 160 L0 160 Z). Prima si scopre con clip-path inset dall'alto; poi MorphSVG lo porta alla forma scritta in data-forma. Partire da una forma semplice rende il morph leggibile: si vede il rettangolo piegarsi.
SplitText divide i testi in righe e ogni riga riceve un suo filtro SVG: feGaussianBlur seguito da feColorMatrix con una soglia sull'alfa. Con la sfocatura alta la riga sparisce del tutto; mentre stdDeviation scende a zero compaiono macchie nette che si stringono in lettere. A fine corsa il filtro si toglie.
| Nome | File | Di partenza | Cosa cambia |
|---|---|---|---|
CONTEGGIO | script.js | 2.2 | Secondi per arrivare a cento. In un sito vero legalo al caricamento reale (immagini, font) con un minimo per non farlo lampeggiare. |
SFOCATURA | script.js | 0.32 | La sfocatura iniziale di ogni riga, in frazione della sua altezza. Più alta: le macchie compaiono più tardi e più grosse. |
SOGLIA | script.js | 0 0 0 22 -8 | L'ultima riga della matrice che taglia l'alfa. Il primo numero indurisce i bordi, il secondo sposta il punto in cui la macchia compare. |
data-forma | index.html | il germoglio | Il path finale del marchio: metti il tuo, disegnato nello stesso riquadro 96 x 160. |
Per l'ingresso di un portfolio o di un sito di marca, alla prima visita. Non a ogni pagina: la seconda volta deve bastare il marchio già al suo posto.
Le colonne si stringono a uno contro tre; conteggio, marchio e righe restano identici. I filtri SVG su testi grandi pesano: tienili solo per l'ingresso.
Niente conteggio: marchio, pulsanti e testi sono già al loro posto.
GSAP 3.15.0, MorphSVG (GSAP) 3.15.0, SplitText (GSAP) 3.15.0, Mona Sans (font variabile) 5.3.0.
Peso: 12 KB dei nostri file.
Provata il 26 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.