Vai al contenuto
animorio
EN
Tutti i componenti

La nav giustificata

Navigazione, Hover, TestoGSAP, Solo CSSImpegnativaPeso compresso: Demo 9,8 KBGSAP 28 KBGratisScarica lo zip

La barra in alto di una piccola fonderia di caratteri, nello stile minimal: due filetti sottili e una riga di voci che riempie sempre la stessa misura, come una riga giustificata. Passando su una voce, quella si allarga e prende peso sull'asse della larghezza del carattere, e le altre si stringono quanto basta perché la riga resti lunga uguale, ognuna sulla sua molla. La pagina in cui si è resta un poco più larga e un segno vermiglio sottile scende sopra di lei dal filetto. Al telefono le voci si aprono in una colonna che tiene la stessa altezza allo stesso modo.

Il codice

Una cartella che funziona da sola: index.html, style.css, script.js e le librerie da CDN. Nelle schede React e Vue la stessa demo è già un componente, che spegne le sue animazioni quando esce dalla pagina.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The justified nav</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/roboto-flex@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the top bar of a small type foundry, Tympan. Its links fill the same measure every time, like a justified line:
         the link under the pointer widens and takes weight on the font's width axis, the others narrow just enough for
         the line to stay exactly as long. The current page stays a little wider and a thin vermilion tick hangs over
         it from the top rule. On a phone the links open in a column that keeps its height the same way -->
    <header class="bar" data-bar data-autoclic>
      <span class="bar__rule bar__rule--top" aria-hidden="true" data-rule></span>
      <span class="bar__rule bar__rule--bottom" aria-hidden="true" data-rule></span>

      <a class="brand" href="#" aria-label="Tympan, home" data-brand><span class="brand__word">Tympan</span></a>

      <button class="toggle" type="button" aria-expanded="false" aria-controls="tympan-links" data-toggle>
        <span class="toggle__label" data-toggle-label>Menu</span>
        <svg class="toggle__icon" viewBox="0 0 20 20" aria-hidden="true">
          <path class="toggle__line" d="M3 7.5h14" data-line/>
          <path class="toggle__line" d="M3 12.5h14" data-line/>
        </svg>
      </button>

      <nav class="nav" id="tympan-links" aria-label="Main" data-nav>
        <span class="nav__end nav__end--start" aria-hidden="true" data-end></span>
        <span class="nav__end nav__end--end" aria-hidden="true" data-end></span>
        <ul class="list" data-list>
          <li class="item"><a class="link" href="#" aria-current="page" data-link><span class="link__mark" aria-hidden="true"><span class="link__tick" data-tick></span></span><span class="link__word" data-word>Typefaces</span></a></li>
          <li class="item"><a class="link" href="#" data-link><span class="link__mark" aria-hidden="true"><span class="link__tick" data-tick></span></span><span class="link__word" data-word>Trials</span></a></li>
          <li class="item"><a class="link" href="#" data-link><span class="link__mark" aria-hidden="true"><span class="link__tick" data-tick></span></span><span class="link__word" data-word>Licensing</span></a></li>
          <li class="item"><a class="link" href="#" data-link><span class="link__mark" aria-hidden="true"><span class="link__tick" data-tick></span></span><span class="link__word" data-word>Custom</span></a></li>
          <li class="item"><a class="link" href="#" data-link><span class="link__mark" aria-hidden="true"><span class="link__tick" data-tick></span></span><span class="link__word" data-word>Journal</span></a></li>
          <li class="item"><a class="link" href="#" data-link><span class="link__mark" aria-hidden="true"><span class="link__tick" data-tick></span></span><span class="link__word" data-word>Studio</span></a></li>
        </ul>
      </nav>
    </header>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Cambia valori, colori e testi con le manopole e guarda l’animazione rispondere dal vivo. Oppure scrivi tu nel codice. Poi scarica la cartella con le tue modifiche.

23 valori da cambiare

Oppure chiedila a Claude Code

Col connettore di Animorio, Claude Code la prende da qui con tutta la sua guida e la adatta al tuo progetto. Incolla questa richiesta:

Prendi «La nav giustificata» da Animorio e adattala al mio sito

Cosa fa

Su un fondo bianco una barra alta 60 pixel, larga 660, fatta solo da due filetti neri sottili, uno sopra e uno sotto. A sinistra il nome Tympan, nero e stretto; poi sei voci in Roboto Flex: Typefaces, Trials, Licensing, Custom, Journal e Studio. La prima parola tocca l'inizio della misura e l'ultima la sua fine, segnati da due tacche grigie appoggiate al filetto di sotto, e gli spazi fra le parole sono tutti uguali. All'arrivo i filetti si disegnano da sinistra, il nome compare e le voci si aprono una dopo l'altra, da strettissime fino a riempire la riga, andando appena oltre e tornando; poi dal filetto di sopra scende un segno vermiglio sopra Typefaces, la pagina in cui si è. Col mouse su una voce, quella diventa nera, si allarga e prende peso; le altre si stringono tutte quanto serve perché la riga resti lunga esattamente uguale, prima le vicine e un attimo dopo le lontane, e gli spazi respirano e si assestano. Scorrendo da una voce all'altra la parola larga passa dall'una all'altra. Cliccando una voce il segno sopra la vecchia si ritira nel filetto e uno nuovo scende sopra la nuova, un pixel troppo lungo, e si assesta; la voce scelta resta un poco più larga e più piena delle altre. Da tastiera la voce col fuoco si allarga come col mouse e ha una cornice sottile intorno, che si allarga con lei; le frecce passano da una voce all'altra.

Come funziona

Quanto è larga ogni parola, misurato una volta

Roboto Flex ha l'asse della larghezza da 25 a 151 e quello del peso da 100 a 1000. All'avvio e a ogni cambio di misura lo script dà a tutte le parole, una dopo l'altra, i valori di una griglia (sei larghezze per quattro pesi) e legge ogni volta getBoundingClientRect().width: 24 letture in tutto. Fra un punto e l'altro della griglia la larghezza si ricava con due interpolazioni lineari, e non si misura più niente durante il movimento.

La riga giustificata: una bisezione a ogni fotogramma

Ogni voce ha una larghezza in più (HOVER_WIDTH sotto il puntatore, CURRENT_WIDTH se è la pagina corrente) e un peso. A ogni fotogramma una bisezione di 28 passi cerca la larghezza di base b per cui la somma delle parole, ognuna a b più la sua aggiunta, è esattamente la misura meno gli spazi (GAP per ogni spazio). La lista è un flex con justify-content: space-between, quindi la prima parola sta all'inizio e l'ultima alla fine: se l'asse arriva in fondo alla corsa, è lo spazio fra le parole a prendersi il resto, come in una riga di tipografia.

Le molle e l'onda che parte dalla voce

Quanto una voce è sotto il puntatore e quanto è la pagina corrente vanno da 0 a 1 su molle smorzate (SPRING), con passi di al massimo 1/120 di secondo e mai più di 1/30 per fotogramma: la parola che si allarga va un poco oltre e torna. Anche la base di ogni parola insegue quella della bisezione con una sua molla, più morbida quanto più è lontana dalla voce che è cambiata (RIPPLE): le vicine si stringono subito, le lontane un attimo dopo, e intanto gli spazi respirano. Se le molle insieme allungassero troppo la riga, una seconda bisezione toglie a tutte la stessa larghezza finché gli spazi non scendono sotto il minimo.

Il puntatore che non salta

Non conta la parola sotto il puntatore, ma la metà dello spazio: la riga è tagliata a metà di ogni spazio fra due parole. Una parola che si allarga sotto il puntatore si porta dietro i suoi confini, quindi lo tiene finché il puntatore non supera la metà dello spazio dopo: niente avanti e indietro fra due voci quando la riga si sposta. Fuori dalla barra la parola larga torna stretta; il fuoco da tastiera fa lo stesso del puntatore.

Il segno della pagina corrente

Ogni voce ha un segno vermiglio largo 2 pixel appeso al filetto di sopra, al centro della parola: un elemento per la posizione (left: 50%, del CSS) e il suo figlio per GSAP, che lo fa scendere con scaleY e un back.out (un pixel di troppo e il ritorno) e lo ritira con un power2.in. Il segno sta dentro la voce, quindi segue la parola mentre la riga si giustifica. La voce scelta prende aria-current="page".

La colonna al telefono

Sotto i 760 pixel le stesse voci finiscono in un pannello sotto la barra, che si apre col pulsante Menu con un clip-path che scende. L'altezza del pannello non cambia mai (sei righe da 46): la riga sotto il dito si alza di ROW_GROW pixel e le altre cedono lo spazio, la pagina corrente ne tiene un terzo; il carattere della riga cresce con lei, si allarga e prende peso, e se non ci sta prima si stringe e poi rimpicciolisce. Il dito che scorre giù per la colonna sposta la riga grande; alzandolo si sceglie quella voce e il pannello si chiude. Il segno è lo stesso, girato: esce dal bordo sinistro della riga.

Valori da cambiare

NomeFileDi partenzaCosa cambia
HOVER_WIDTHscript.js56Quanto si allarga la voce sotto il puntatore, in unità dell'asse della larghezza (che va da 25 a 151).
HOVER_WEIGHTscript.js720Il peso della voce sotto il puntatore (da 100 a 1000).
CURRENT_WIDTHscript.js10Quanto resta più larga la pagina in cui si è, sull'asse della larghezza.
CURRENT_WEIGHTscript.js540Il peso della pagina in cui si è.
REST_WEIGHTscript.js380Il peso delle altre voci.
GAPscript.js38Lo spazio che le parole tengono fra loro, in pixel: il resto lo prende l'asse della larghezza. Più grande, voci più strette.
SPRINGscript.js[230, 22]La molla di ogni voce: quanto è rigida e quanto è frenata.
RIPPLEscript.js0.35Quanto rispondono dopo le voci più lontane da quella che cambia (0, tutte insieme).
ROW_GROWscript.js28Al telefono, di quanti pixel si alza la riga sotto il dito (le altre cedono lo spazio).

Provali nel Playground

Quando usarla

Per la testata di chi ha a che fare coi caratteri e con le parole: una fonderia di caratteri, uno studio di grafica, una rivista, una casa editrice, il portfolio di un tipografo o di un designer. Sta bene con cinque, sei o sette voci corte su una barra chiara e vuota; con voci molto lunghe l'asse arriva presto in fondo e lavora solo lo spazio. Colori e pesi si cambiano nelle variabili.

Sul telefono

Sotto i 760 pixel la barra è alta 56 pixel e larga quanto lo schermo meno 32 (358 a 390): a sinistra il nome, a destra il pulsante Menu con due linee sottili. Toccato, le linee diventano una croce, la scritta dice Close e sotto la barra scende un pannello bianco con le sei voci in colonna, separate da linee sottili: le parole entrano una dopo l'altra, da strette fino alla loro larghezza, e il segno vermiglio esce dal bordo sinistro della pagina corrente. Il pannello è sempre alto uguale: col dito su una riga quella si alza, la parola diventa grande, larga e piena, e le altre righe si stringono per farle posto; facendo scorrere il dito su e giù la riga grande lo segue. Alzando il dito si sceglie quella voce, il segno scende sulla nuova e il pannello si chiude. Esc chiude e riporta il fuoco sul pulsante.

Con il movimento ridotto

Niente molle e niente parole che si allargano: la voce sotto il puntatore diventa solo nera, la pagina corrente resta già un poco più larga e la riga è già giustificata. Scegliendo un'altra voce la riga si rifà al suo posto in una breve dissolvenza e il segno vermiglio sfuma sulla vecchia e ricompare sulla nuova, senza scendere. All'apertura tutto compare in dissolvenza; al telefono il pannello compare e sparisce con una dissolvenza, senza scendere, e le righe restano alte uguali.

Trappole

  • La prima riga lasciava a riposo parole a larghezza 151, in fondo all'asse, con spazi di cento pixel: Roboto Flex a 16 pixel è più stretto di quanto sembra. La misura ora è di 660 pixel, il carattere di 17 e lo spazio di 38, così a riposo la base sta intorno a 96 e la voce sotto il puntatore ha posto per allargarsi.
  • La larghezza delle parole non cresce in modo lineare con l'asse: con lo spazio a 40 la base a riposo era 79 (voci strette e grigie), a 36 già 107, e la voce sotto il puntatore arrivava in fondo all'asse. Per questo la griglia ha sei punti sulla larghezza e quattro sul peso invece dei soli estremi, e misurati sulla pagina gli spazi veri sono 37,92 pixel contro i 38 chiesti; il poco che resta lo assorbe space-between.
  • Scegliendo la voce sotto il puntatore guardando la parola più vicina, la parola che si allargava spostava il suo centro e il puntatore passava alla vicina, poi tornava: un tremolio. Ora i confini stanno a metà degli spazi e si muovono con le parole.
  • Con le molle tutte insieme in rimbalzo la riga diventava per un attimo più lunga della misura e gli spazi si chiudevano: una seconda bisezione toglie a tutte la stessa larghezza finché gli spazi non restano almeno al 45% di GAP.
  • Il giro dell'anteprima e il tasto Invio fanno tutti e due un clic senza dettagli (detail 0): Invio da tastiera faceva partire il giro. Ora un Invio o uno Spazio appena premuti sulla voce la scelgono subito.
  • Le parole del pannello del telefono restavano invisibili: la loro entrata parte con un ritardo, e il ciclo dei fotogrammi si fermava perché in quel momento nessuna animazione era ancora in corso. Ora l'entrata lo risveglia a ogni passo.

Librerie

GSAP 3.15.0, Roboto Flex (font variabile, con gli assi della larghezza e del peso) 5.3.0.

Compressi con gzip, i tre file della demo pesano 9,8 KB; le librerie: GSAP 28 KB.

Collaudo

Provata il 16 ottobre 2026

  • Computer: Fatto
  • Telefono: Fatto
  • Movimento ridotto: Fatto
  • Safari su iPhone: Da fare

Licenza d’uso

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.

Tutti i componenti