Skip to content
animorio
IT
All components

The mailbox alert

Notifications, SVG, TransitionsGSAP, CSS onlyAdvancedCompressed weight: Demo 13 KBGSAP 28 KBFreeDownload the zip

A shop’s alerts, delivered like post, in the rich style: 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 with a stamp and a postmark comes out of the box, flies to its place, turns over and opens, and the letter inside is the alert. Four kinds, each with its own envelope: shipped, payment failed (the box shakes and the envelope arrives torn), new message, saved.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!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

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

58 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The mailbox alert” from Animorio and fit it into my site

What it does

On a white background, on its own, a mailbox of metal painted petrol blue on a wooden post with its grain and a knot: the lid with its hinge on the left, the brass plate reading LETTERS, rivets in the corners, two light dents with their highlight and shadow, a streak of light down the side, and on the right the red flag on its pin, lowered. Under it, four pill buttons: Shipped, Failed, Message, Saved. When an alert arrives the flag springs up and wobbles, the lid swings open on its hinge and a small envelope rises out of the box, in the colour of the kind of alert: green for a shipped order, red for a failed payment, blue for a message, cream for a saved list. The lid claps shut and the envelope flies on an arc to its place, growing, with the address side towards you: the sender, Ada Moreau handwritten, the perforated stamp with a larkspur and the wavy postmark. Once there it turns over (it squeezes and widens again showing the back with its seams), the flap folds up and the letter comes out of the pocket: it is the alert, an ivory sheet with a crease across the middle, the drawn icon, the title, a line and the cross. The envelope drops away and the alert stays. For a failed payment the box shakes on its post and the envelope arrives with a torn corner: the scrap comes off during the flight and falls. After a few seconds, or with the cross or Esc, the letter sinks back into the envelope, the flap closes, the envelope flies back to the box and drops in, the lid closes with a small bounce and the flag swings down. On its own the demo sends the four kinds one after the other.

How it works

Every part turns on its own pin

The mailbox is an SVG: the flag, the lid and the whole box are groups GSAP turns with svgOrigin, that is about a point in the drawing's coordinates: the flag about the pin on the side, the lid about its hinge, the box about the foot of the post when it shakes. So every part stays fixed where it is attached, at any size.

The envelope in two layers, the letter between them

The envelope is made of two layers moving together: behind, the far sheet (and the flap once open); in front, the near side (the address while it flies, the back with its seams and the closed flap once it has turned). The alert sits between them, so it really comes out of the pocket: the near side covers it until it rises over the edge.

The flight on an arc

From the mouth of the box to its place the envelope follows a curve: one variable from 0 to 1 tweened by GSAP, and at every step x linear, y with a sine that lifts it, the scale with an ease that makes it grow early and a rotation that leans in the middle of the flight and straightens on arrival.

Turning over and opening in two dimensions

The envelope turns over by squeezing to zero on the x axis and widening again with a back.out: the faces are swapped at the moment it is as thin as a thread. The closed flap shortens towards the edge (scaleY from the top), and once it is gone the open one grows, behind the letter, from the edge upwards.

The kind of alert and who must hear it

Each kind changes the colour of the envelope and the letter (a CSS variable taken from :root), the SVG icon and the words; the failed payment adds the shake, the torn corner (a zig-zag clip-path) and the falling scrap. The words go to a polite role="status" line, or to a role="alert" one that interrupts when the payment has failed.

Values to change

NameFileDefaultWhat it changes
FLAG_SPRINGscript.js0.42How springy the flag is when it pops up: 0.2 stiff, 0.8 wobbly.
FLIGHTscript.js0.75The seconds the envelope takes to fly from the box to its place.
OPENscript.js0.5The seconds the envelope takes to turn over and open.
HOLDscript.js2.8The seconds an alert stays before it goes back into the box by itself.
GAPscript.js0.7The seconds between two alerts when the demo plays by itself.
SHAKEscript.js3.5How far the box shakes for a failed payment, in degrees.
FIRSTscript.js0.6The seconds before the first alert arrives.
MESSAGESscript.jsquattro avvisi: Order shipped, New message, Payment failed, SavedThe alerts, in turn: the kind, the title, the line and the name on the envelope.

Try them in the Playground

When to use it

For the alerts of an online shop, a shipping or mail app, a service with a handmade touch: order shipped, payment failed, new message, saved. One alert at a time, with a title and a line; the delivery takes about three seconds, so it suits places where alerts do not come in bursts.

On phones

On a phone the alert goes to the top, almost as wide as the box, and the smaller mailbox sits under it on the left; the envelope, the flap and the buttons shrink, so it all fits a 341 by 410 box. The animations are the same.

With reduced motion

With reduced motion no envelopes fly: the flag is already up and the alert fades in at its place, and fades out the same way.

Pitfalls

  • The letter had to come out of the envelope, but an element animated with a transform makes its own stacking context and cannot be slipped between another's children: the envelope became two twin elements, moved together, with the alert between them.
  • SVG gradient stops with stop-color="var(--…)" as an attribute stay black: the stops have a class and the colour lives in the CSS.
  • The near side of the envelope had a thin light line along the top which, with the flap open, looked like a white gap between the flap and the envelope: the back has no such line.
  • On a phone the envelope was wider than the box and the page spilled sideways: the envelope is now at most as wide as the scene, and lower.

Libraries

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

Compressed with gzip, the three demo files weigh 13 KB; the libraries: GSAP 28 KB.

Testing

Tested on 22 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

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.

All components