
Words that melt into each other
Text, Scroll, SVG
NewFree
A count up to a hundred, a rule that grows, then a rectangle that is revealed and turns into the logo while the lines emerge from the ink.
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>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);An object { n: 0 } goes to 100 with the same power3.inOut curve that grows the rule from height 0 to 100%: number and rule arrive together. The number sits in a line with overflow clip, so it enters and leaves its mask with yPercent.
When the count ends the two buttons start 6vw off screen on a diagonal and come back to zero with expo.out. It is a small detail, but it tells you the site is ready.
The path starts as a rectangle (M0 0 L96 0 L96 160 L0 160 Z). First it is revealed with a clip-path inset from the top; then MorphSVG takes it to the shape written in data-forma. Starting from a simple shape makes the morph readable: you see the rectangle bend.
SplitText splits the texts into lines and each line gets its own SVG filter: feGaussianBlur followed by feColorMatrix with a threshold on alpha. With a strong blur the line disappears completely; as stdDeviation goes down to zero, sharp blots appear and tighten into letters. At the end the filter is removed.
| Name | File | Default | What it changes |
|---|---|---|---|
CONTEGGIO | script.js | 2.2 | Seconds to reach a hundred. On a real site tie it to real loading (images, fonts) with a minimum so it never just flashes. |
SFOCATURA | script.js | 0.32 | The initial blur of each line, as a fraction of its height. Higher: the blots appear later and bigger. |
SOGLIA | script.js | 0 0 0 22 -8 | The last row of the matrix that cuts the alpha. The first number hardens the edges, the second moves the point where the blot appears. |
data-forma | index.html | il germoglio | The final path of the logo: put yours, drawn in the same 96 x 160 box. |
For the entrance of a portfolio or a brand site, on the first visit. Not on every page: the second time the logo already in place should be enough.
The columns narrow to one against three; count, logo and lines stay the same. SVG filters on large text are heavy: keep them for the entrance only.
No count: logo, buttons and texts are already in place.
GSAP 3.15.0, MorphSVG (GSAP) 3.15.0, SplitText (GSAP) 3.15.0, Mona Sans (font variabile) 5.3.0.
Weight: 12 KB of our own files.
Tested on 26 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.