Vai al contenuto
animorio
EN
Tutti i componenti

L’avviso della cassetta delle lettere

Notifiche, SVG, TransizioniGSAP, Solo CSSImpegnativaPeso compresso: Demo 13 KBGSAP 28 KBGratisScarica lo zip

Gli avvisi di un negozio, consegnati come posta, nello stile ricco: una cassetta delle lettere di metallo verniciato sul suo palo di legno. Quando arriva un avviso la bandierina rossa scatta su, il coperchio si apre sul cardine, una busta col francobollo e il timbro esce dalla cassetta, vola al suo posto, si gira e si apre, e la lettera dentro è l’avviso. Quattro tipi, ognuno con la sua busta: spedito, pagamento rifiutato (la cassetta trema e la busta arriva strappata), nuovo messaggio, salvato.

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 mailbox alert</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/caveat@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the alerts of a small shop, delivered like post: a painted metal mailbox on its wooden post. When an alert
         arrives the red flag springs up, the lid swings open on its hinge, an envelope (paper grain, a perforated
         stamp, a postmark) rises out of the box, flies to its place and opens, and the letter inside is the alert.
         A failed payment shakes the box and the envelope arrives with a torn corner. The alert closes by itself after
         a few seconds, or with its cross or Esc, and the letter goes back into the envelope and the box -->
    <section class="post" aria-label="Shop alerts" data-post>
      <div class="post__scene" data-scene>
        <svg class="box" viewBox="0 0 200 240" aria-hidden="true" data-box>
          <defs>
            <linearGradient id="mb-paint" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0" class="stop-paint-dark"/>
              <stop offset="0.18" class="stop-paint-light"/>
              <stop offset="0.55" class="stop-paint"/>
              <stop offset="1" class="stop-paint-dark"/>
            </linearGradient>
            <linearGradient id="mb-lid" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" class="stop-paint-light"/>
              <stop offset="1" class="stop-paint-dark"/>
            </linearGradient>
            <linearGradient id="mb-wood" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0" class="stop-wood-dark"/>
              <stop offset="0.35" class="stop-wood-light"/>
              <stop offset="1" class="stop-wood-dark"/>
            </linearGradient>
            <linearGradient id="mb-brass" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" class="stop-brass-light"/>
              <stop offset="0.5" class="stop-brass"/>
              <stop offset="1" class="stop-brass-dark"/>
            </linearGradient>
            <linearGradient id="mb-flag" x1="0" y1="0" x2="1" y2="1">
              <stop offset="0" class="stop-flag-light"/>
              <stop offset="1" class="stop-flag"/>
            </linearGradient>
            <linearGradient id="mb-steel" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0" class="stop-steel-dark"/>
              <stop offset="0.5" class="stop-steel-light"/>
              <stop offset="1" class="stop-steel-dark"/>
            </linearGradient>
            <radialGradient id="mb-rivet" cx="0.35" cy="0.3" r="0.8">
              <stop offset="0" class="stop-steel-light"/>
              <stop offset="1" class="stop-steel-dark"/>
            </radialGradient>
            <filter id="mb-soft" x="-30%" y="-200%" width="160%" height="500%">
              <feGaussianBlur stdDeviation="4"/>
            </filter>
          </defs>

          <!-- the shadow on the ground -->
          <ellipse class="box__ground" cx="100" cy="232" rx="66" ry="6" filter="url(#mb-soft)"/>

          <!-- the wooden post, its grain and the bracket the box sits on -->
          <g class="box__post">
            <rect x="89" y="148" width="22" height="88" rx="2" fill="url(#mb-wood)"/>
            <path class="box__grain" d="M95 152 C 94 172, 97 190, 95 212 S 96 228, 95 236"/>
            <path class="box__grain" d="M103 150 C 104 168, 101 186, 104 204 S 103 224, 105 236"/>
            <path class="box__grain box__grain--knot" d="M99 196 c 3 -3 5 1 2 4 c -3 2 -5 -1 -2 -4 z"/>
            <rect x="62" y="146" width="76" height="9" rx="2" fill="url(#mb-wood)"/>
            <rect x="62" y="153" width="76" height="2" class="box__bracket-shade"/>
            <circle cx="70" cy="150.5" r="1.6" fill="url(#mb-rivet)"/>
            <circle cx="130" cy="150.5" r="1.6" fill="url(#mb-rivet)"/>
          </g>

          <!-- the box: the shake of a failed payment turns it about the foot of the post -->
          <g class="box__body" data-body>
            <!-- the dark slot under the lid, seen when it opens -->
            <rect class="box__mouth" x="48" y="56" width="104" height="9" rx="3"/>
            <rect x="40" y="60" width="120" height="90" rx="12" fill="url(#mb-paint)"/>
            <rect class="box__bevel" x="44.5" y="64.5" width="111" height="81" rx="9"/>
            <path class="box__band" d="M40 132 h120 v6 a12 12 0 0 1 -12 12 h-96 a12 12 0 0 1 -12 -12 z"/>
            <path class="box__shine" d="M50 70 C 49 92, 49 118, 52 140" />
            <path class="box__shine box__shine--thin" d="M57 72 C 56 92, 56 112, 58 128" />
            <!-- two soft dents, a light rim and a shadow each -->
            <ellipse class="box__dent-shade" cx="132" cy="98" rx="7" ry="4.5" transform="rotate(-18 132 98)"/>
            <ellipse class="box__dent-light" cx="130.5" cy="96" rx="5.5" ry="3" transform="rotate(-18 130.5 96)"/>
            <ellipse class="box__dent-shade" cx="62" cy="128" rx="5" ry="3" transform="rotate(12 62 128)"/>
            <ellipse class="box__dent-light" cx="61" cy="126.6" rx="4" ry="2" transform="rotate(12 61 126.6)"/>
            <!-- the brass plate -->
            <rect x="70" y="102" width="60" height="18" rx="3" fill="url(#mb-brass)"/>
            <rect class="box__plate-edge" x="71.5" y="103.5" width="57" height="15" rx="2"/>
            <text class="box__plate-word" x="100" y="114.6" text-anchor="middle">LETTERS</text>
            <circle cx="74.5" cy="111" r="1.4" fill="url(#mb-rivet)"/>
            <circle cx="125.5" cy="111" r="1.4" fill="url(#mb-rivet)"/>
            <!-- rivets on the corners of the front panel -->
            <circle cx="49" cy="73" r="1.9" fill="url(#mb-rivet)"/>
            <circle cx="151" cy="73" r="1.9" fill="url(#mb-rivet)"/>
            <circle cx="49" cy="141" r="1.9" fill="url(#mb-rivet)"/>
            <circle cx="151" cy="141" r="1.9" fill="url(#mb-rivet)"/>

            <!-- the lid, hinged on its left pin -->
            <g class="box__lid" data-lid>
              <rect x="35" y="50" width="130" height="14" rx="5" fill="url(#mb-lid)"/>
              <rect class="box__lid-lip" x="35" y="60" width="130" height="4" rx="2"/>
              <path class="box__lid-shine" d="M44 53.5 H 150"/>
              <rect x="150" y="56" width="10" height="5" rx="1.5" fill="url(#mb-steel)"/>
            </g>
            <rect x="31" y="53" width="9" height="9" rx="2" fill="url(#mb-steel)"/>
            <circle cx="35.5" cy="57.5" r="1.7" fill="url(#mb-rivet)"/>

            <!-- the flag, pivoting on a pin on the right side of the box -->
            <g class="box__flag" data-flag>
              <rect x="164.5" y="54" width="5" height="70" rx="2.5" fill="url(#mb-steel)"/>
              <path d="M169 54 C 180 51, 188 57, 197 54 L 197 74 C 188 77, 180 71, 169 74 Z" fill="url(#mb-flag)"/>
              <path class="box__flag-fold" d="M180 53.5 C 181 60, 181 66, 180 72.5"/>
              <path class="box__flag-shine" d="M172 58 C 180 56, 186 60, 193 58"/>
            </g>
            <circle class="box__pin" cx="167" cy="120" r="3.6" fill="url(#mb-rivet)"/>
            <circle class="box__pin-dot" cx="167" cy="120" r="1.1"/>

            <!-- the point the envelope comes out of -->
            <rect class="box__mark" x="99" y="60" width="2" height="2" data-mouth/>
          </g>
        </svg>

        <!-- the envelope is two layers with the letter between them: the far sheet (and the flap once it is open)
             behind the alert, the near side (the address side while it flies, the back with its seams and closed
             flap once it has turned) in front of it -->
        <div class="env env--far" aria-hidden="true" data-env-far>
          <div class="env__sheet"></div>
          <div class="env__flap-open" data-flap-open></div>
        </div>
        <!-- the alert itself: the letter. Its role and words change with each kind -->
        <div class="toast" data-toast>
          <div class="toast__sheet">
            <span class="toast__icon" data-icon aria-hidden="true"></span>
            <span class="toast__text">
              <span class="toast__title" data-title>Order shipped</span>
              <span class="toast__body" data-body-text>Two linen shirts are on their way.</span>
            </span>
            <button class="toast__close" type="button" aria-label="Dismiss alert" data-close>
              <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 7l10 10M17 7L7 17"/></svg>
            </button>
          </div>
        </div>

        <div class="env env--near" aria-hidden="true" data-env-near>
          <div class="env__face env__face--front" data-face-front>
            <span class="env__from">Larkspur Goods<br>Mill Lane</span>
            <span class="env__to" data-to>Ada Moreau</span>
            <span class="env__lines"></span>
            <svg class="env__stamp" viewBox="0 0 36 42">
              <defs>
                <mask id="mb-perf" maskUnits="userSpaceOnUse" x="0" y="0" width="36" height="42">
                  <rect x="0" y="0" width="36" height="42" fill="#fff"/>
                  <g fill="#000">
                    <circle cx="3" cy="0" r="1.7"/><circle cx="9" cy="0" r="1.7"/><circle cx="15" cy="0" r="1.7"/><circle cx="21" cy="0" r="1.7"/><circle cx="27" cy="0" r="1.7"/><circle cx="33" cy="0" r="1.7"/>
                    <circle cx="3" cy="42" r="1.7"/><circle cx="9" cy="42" r="1.7"/><circle cx="15" cy="42" r="1.7"/><circle cx="21" cy="42" r="1.7"/><circle cx="27" cy="42" r="1.7"/><circle cx="33" cy="42" r="1.7"/>
                    <circle cx="0" cy="3" r="1.7"/><circle cx="0" cy="9" r="1.7"/><circle cx="0" cy="15" r="1.7"/><circle cx="0" cy="21" r="1.7"/><circle cx="0" cy="27" r="1.7"/><circle cx="0" cy="33" r="1.7"/><circle cx="0" cy="39" r="1.7"/>
                    <circle cx="36" cy="3" r="1.7"/><circle cx="36" cy="9" r="1.7"/><circle cx="36" cy="15" r="1.7"/><circle cx="36" cy="21" r="1.7"/><circle cx="36" cy="27" r="1.7"/><circle cx="36" cy="33" r="1.7"/><circle cx="36" cy="39" r="1.7"/>
                  </g>
                </mask>
              </defs>
              <g mask="url(#mb-perf)">
                <rect class="stamp__paper" x="0" y="0" width="36" height="42"/>
                <rect class="stamp__field" x="4" y="4" width="28" height="34" rx="1"/>
                <!-- a larkspur: a stem with its little spurred flowers and two leaves -->
                <path class="stamp__stem" d="M18 35 C 18 27, 17.5 20, 18.5 10"/>
                <path class="stamp__leaf" d="M18 30 C 13 28, 11 25, 11 23 C 14 24, 17 26, 18 30 Z"/>
                <path class="stamp__leaf" d="M18.2 27 C 23 25, 25 22, 25.5 20 C 22 21, 19 23.5, 18.2 27 Z"/>
                <path class="stamp__bloom" d="M18.5 10 c -3 -1 -3 -4 0 -4 c 3 0 3 3 0 4 z M17 15 c -3 0 -4 -3 -1 -4 c 2 0 3 2 1 4 z M20 19 c 3 0 4 -3 1 -4 c -2 0 -3 2 -1 4 z M17 23 c -3 0 -4 -3 -1 -3.5 c 2 0 2.5 2 1 3.5 z"/>
              </g>
            </svg>
            <svg class="env__postmark" viewBox="0 0 64 40">
              <circle cx="20" cy="20" r="15"/>
              <circle cx="20" cy="20" r="11"/>
              <path d="M38 12 c 4 -3 8 3 12 0 s 8 3 12 0 M38 20 c 4 -3 8 3 12 0 s 8 3 12 0 M38 28 c 4 -3 8 3 12 0 s 8 3 12 0"/>
              <text x="20" y="22.6" text-anchor="middle">LARK</text>
            </svg>
          </div>
          <div class="env__face env__face--back" data-face-back>
            <svg class="env__seams" viewBox="0 0 100 50" preserveAspectRatio="none">
              <path d="M0 50 L 42 27 M100 50 L 58 27"/>
            </svg>
            <div class="env__flap-shut" data-flap-shut></div>
          </div>
          <div class="env__scrap" data-scrap></div>
        </div>
      </div>

      <!-- the buttons that send each kind of alert -->
      <div class="post__send" role="group" aria-label="Send an alert">
        <button class="chip" type="button" data-send="shipped">
          <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8.5 12 4l8 4.5v7L12 20l-8-4.5z M4 8.5 12 13l8-4.5 M12 13v7"/></svg>
          <span>Shipped</span>
        </button>
        <button class="chip" type="button" data-send="failed">
          <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3.5 7h17v10h-17z M3.5 10.5h17 M7 14.5h4 M15 13l4 4 M19 13l-4 4"/></svg>
          <span>Failed</span>
        </button>
        <button class="chip" type="button" data-send="message">
          <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 6.5h15v9h-8l-4 3.5v-3.5h-3z"/></svg>
          <span>Message</span>
        </button>
        <button class="chip" type="button" data-send="saved">
          <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 4.5h10v15l-5-3.6-5 3.6z"/></svg>
          <span>Saved</span>
        </button>
      </div>

      <!-- what screen readers hear: the calm kinds politely, the failed payment at once -->
      <p class="sr" role="status" aria-live="polite" data-live-status></p>
      <p class="sr" role="alert" aria-live="assertive" data-live-alert></p>
    </section>
  </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.

58 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 «L’avviso della cassetta delle lettere» da Animorio e adattala al mio sito

Cosa fa

Su un fondo bianco, da sola, una cassetta delle lettere di metallo verniciato blu petrolio su un palo di legno con le venature e un nodo: il coperchio col cardine a sinistra, la targa d’ottone con la scritta LETTERS, i rivetti agli angoli, due ammaccature leggere con la loro luce e la loro ombra, una riga di luce sul fianco, e a destra la bandierina rossa sul suo perno, abbassata. Sotto, quattro pulsanti a pillola: Shipped, Failed, Message, Saved. Quando arriva un avviso la bandierina scatta su con una molla e oscilla, il coperchio si apre sul cardine e dalla cassetta sale una busta piccola, del colore del tipo d’avviso: verde per un ordine spedito, rossa per un pagamento rifiutato, blu per un messaggio, crema per una lista salvata. Il coperchio si richiude con un colpo e la busta vola su un arco al suo posto, ingrandendosi, col lato dell’indirizzo verso di te: il mittente, Ada Moreau scritto a mano, il francobollo dentellato con un fiore di speronella e il timbro postale a onde. Arrivata si gira (si stringe e si riallarga mostrando il retro con le cuciture), la patta si apre verso l’alto e la lettera esce dalla tasca: è l’avviso, un foglio color avorio con la piega in mezzo, l’icona disegnata, il titolo, una riga e la croce. La busta cade via e resta l’avviso. Per un pagamento rifiutato la cassetta trema sul palo e la busta arriva con un angolo strappato: il pezzo si stacca durante il volo e cade. Dopo qualche secondo, o con la croce o con Esc, la lettera rientra nella busta, la patta si chiude, la busta torna in volo alla cassetta e ci cade dentro, il coperchio si chiude con un piccolo rimbalzo e la bandierina torna giù. Da sola la demo manda i quattro tipi uno dopo l’altro.

Come funziona

Ogni pezzo gira sul suo perno

La cassetta è un SVG: la bandierina, il coperchio e la cassetta intera sono gruppi che GSAP ruota con svgOrigin, cioè attorno a un punto nelle coordinate del disegno: la bandierina attorno al perno sul fianco, il coperchio attorno al cardine, la cassetta attorno al piede del palo quando trema. Così ogni pezzo resta attaccato dove è fissato, a qualsiasi misura.

La busta in due strati, la lettera in mezzo

La busta è fatta di due livelli che si muovono insieme: dietro il foglio lontano (e la patta quando è aperta), davanti il lato vicino (l’indirizzo mentre vola, il retro con le cuciture e la patta chiusa dopo che si è girata). L’avviso sta in mezzo, quindi esce davvero dalla tasca: il lato vicino lo copre finché sale sopra il bordo.

Il volo su un arco

Dalla bocca della cassetta al suo posto la busta segue una curva: una sola variabile da 0 a 1 animata da GSAP, e a ogni passo x lineare, y con un seno che la solleva, la scala con un ease che la fa crescere presto e una rotazione che si inclina in mezzo al volo e si raddrizza all’arrivo.

Girarsi e aprirsi in due dimensioni

La busta si gira stringendosi a zero sull’asse x e riallargandosi con un back.out: nel momento in cui è sottile come un filo si scambiano le facce. La patta chiusa si accorcia verso il bordo (scaleY dall’alto), e quando è sparita cresce quella aperta, dietro la lettera, dal bordo in su.

Il tipo d’avviso e chi lo deve sentire

Ogni tipo cambia il colore della busta e della lettera (una variabile CSS presa da :root), l’icona SVG e le parole; il pagamento rifiutato aggiunge il tremito, l’angolo strappato (un clip-path a zig-zag) e il pezzo che cade. Le parole vanno a una riga role="status" educata, o a una role="alert" che interrompe se il pagamento è rifiutato.

Valori da cambiare

NomeFileDi partenzaCosa cambia
FLAG_SPRINGscript.js0.42Quanto è molleggiata la bandierina quando scatta su: 0.2 rigida, 0.8 ballerina.
FLIGHTscript.js0.75I secondi del volo della busta dalla cassetta al suo posto.
OPENscript.js0.5I secondi in cui la busta si gira e si apre.
HOLDscript.js2.8I secondi in cui l’avviso resta prima di tornare da solo nella cassetta.
GAPscript.js0.7I secondi fra un avviso e l’altro quando la demo va da sola.
SHAKEscript.js3.5Quanto trema la cassetta per un pagamento rifiutato, in gradi.
FIRSTscript.js0.6I secondi prima che arrivi il primo avviso.
MESSAGESscript.jsquattro avvisi: Order shipped, New message, Payment failed, SavedGli avvisi, a turno: il tipo, il titolo, la riga e il nome sulla busta.

Provali nel Playground

Quando usarla

Per gli avvisi di un negozio online, di un’app di spedizioni o di posta, di un servizio con un tocco artigianale: ordine spedito, pagamento rifiutato, messaggio nuovo, salvataggio riuscito. Un avviso alla volta, con un titolo e una riga; la consegna dura circa tre secondi, quindi sta meglio dove gli avvisi non arrivano a raffica.

Sul telefono

Sul telefono l’avviso va in alto, largo quasi quanto la casella, e la cassetta più piccola sta sotto a sinistra; la busta, la patta e i pulsanti si stringono, così tutto sta in una casella di 341 per 410. Le animazioni sono le stesse.

Con il movimento ridotto

Col movimento ridotto non volano buste: la bandierina è già su e l’avviso compare in dissolvenza al suo posto, e sparisce allo stesso modo.

Trappole

  • La lettera doveva uscire dalla busta, ma un elemento animato con un transform crea un suo contesto di sovrapposizione e non si può infilare fra i figli di un altro: la busta è diventata due elementi gemelli, mossi insieme, con l’avviso in mezzo.
  • Gli stop delle sfumature SVG con stop-color="var(--…)" come attributo restano neri: gli stop hanno una classe e il colore sta nel CSS.
  • Il lato vicino della busta aveva un filo di luce in alto che, con la patta aperta, sembrava una fessura bianca fra la patta e la busta: sul retro il filo non c’è.
  • Sul telefono la busta era più larga della casella e la pagina sforava di lato: la busta ora è larga al massimo quanto la scena, e più bassa.

Librerie

GSAP 3.15.0, Instrument Sans (font variabile) 5.3.0, Caveat (font variabile) 5.3.0.

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

Collaudo

Provata il 22 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