Skip to content
animorio
IT
All components

The toaster button

Buttons, SVG, HoverGSAPMediumCompressed weight: Demo 9.0 KBGSAP 28 KBFreeDownload the zip

The Order breakfast button of a small café, in the rich style: in the round end of the jam-red pill stands a mint enamel toaster with a chrome top, a browning dial and a black lever, with two slices of bread in the slots. On a click the lever goes down, the bread sinks, the slots glow orange and the pill fills like a timer; then the lever snaps up, the toast jumps out golden, turns in the air, drops back with a few crumbs and the words turn to Breakfast's up.

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 toaster button</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the toaster button: the Order breakfast of a small café, with a mint enamel toaster standing in the round left
         end of the pill. Two slices of bread wait in the slots. A click pushes the lever down and the bread sinks; the
         slots glow orange, the heat shimmers, the dial turns back and the pill fills like a timer. Then the lever snaps
         up, the toast jumps out golden, turns in the air and drops back into the slots with a few crumbs, and the words
         turn to Breakfast's up. A second click puts fresh bread back -->
    <div class="tb" data-tb>
      <button class="tb__btn" type="button" data-btn data-autoclic>
        <span class="tb__fill" aria-hidden="true"><span class="tb__timer" data-timer></span></span>
        <span class="tb__faces">
          <span class="tb__face" data-face="rest">
            <span class="tb__word" data-label-text>Order breakfast</span>
            <span class="tb__note" data-note-text>Toast &amp; jam</span>
          </span>
          <span class="tb__face tb__face--busy" data-face="busy" aria-hidden="true">
            <span class="tb__shine" data-busy-text>Toasting…</span>
          </span>
          <span class="tb__face tb__face--done" data-face="done" aria-hidden="true">
            <svg class="tb__tick" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M3 8.6 L6.6 12 L13 4.4" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
            <span class="tb__done" data-done-text>Breakfast's up</span>
          </span>
        </span>
      </button>

      <svg class="tb__art" viewBox="0 0 132 170" width="132" height="170" aria-hidden="true" data-art>
        <defs>
          <linearGradient id="tb-enamel" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" style="stop-color:var(--mint-light)"/>
            <stop offset="0.55" style="stop-color:var(--mint)"/>
            <stop offset="1" style="stop-color:var(--mint-deep)"/>
          </linearGradient>
          <linearGradient id="tb-enamel-side" x1="0" y1="0" x2="1" y2="0">
            <stop offset="0" stop-color="#000" stop-opacity="0.16"/>
            <stop offset="0.12" stop-color="#000" stop-opacity="0"/>
            <stop offset="0.86" stop-color="#000" stop-opacity="0"/>
            <stop offset="1" stop-color="#000" stop-opacity="0.22"/>
          </linearGradient>
          <linearGradient id="tb-top" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" style="stop-color:var(--chrome-light)"/>
            <stop offset="1" style="stop-color:var(--chrome)"/>
          </linearGradient>
          <linearGradient id="tb-chrome" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" style="stop-color:var(--chrome-light)"/>
            <stop offset="0.45" style="stop-color:var(--chrome)"/>
            <stop offset="0.5" style="stop-color:var(--chrome-dark)"/>
            <stop offset="1" style="stop-color:var(--chrome-light)"/>
          </linearGradient>
          <linearGradient id="tb-slot" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" style="stop-color:var(--ink)"/>
            <stop offset="1" style="stop-color:var(--slot)"/>
          </linearGradient>
          <linearGradient id="tb-glow" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" style="stop-color:var(--glow-hot)"/>
            <stop offset="1" style="stop-color:var(--glow)"/>
          </linearGradient>
          <radialGradient id="tb-heat" cx="0.5" cy="0.5" r="0.5">
            <stop offset="0" style="stop-color:var(--glow)" stop-opacity="0.45"/>
            <stop offset="1" style="stop-color:var(--glow)" stop-opacity="0"/>
          </radialGradient>
          <linearGradient id="tb-bread" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" style="stop-color:var(--bread-light)"/>
            <stop offset="1" style="stop-color:var(--bread)"/>
          </linearGradient>
          <radialGradient id="tb-brown" cx="0.5" cy="0.42" r="0.62">
            <stop offset="0" style="stop-color:var(--toast-light)"/>
            <stop offset="0.7" style="stop-color:var(--toast)"/>
            <stop offset="1" style="stop-color:var(--toast-deep)"/>
          </radialGradient>
          <linearGradient id="tb-knob" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" stop-color="#4a3a33"/>
            <stop offset="1" style="stop-color:var(--ink)"/>
          </linearGradient>
          <clipPath id="tb-slotline"><rect x="-20" y="-80" width="172" height="161"/></clipPath>
          <symbol id="tb-slice" viewBox="0 0 30 46" overflow="visible">
            <path d="M3 46 V16 C-1 12 0 3.4 7.4 2.2 C10.6 -0.6 19.4 -0.6 22.6 2.2 C30 3.4 31 12 27 16 V46 Z" style="fill:var(--crust)"/>
            <path d="M5.6 46 V15.4 C2.6 12.4 3.4 5.6 9 4.8 C11.6 2.6 18.4 2.6 21 4.8 C26.6 5.6 27.4 12.4 24.4 15.4 V46 Z" fill="url(#tb-bread)"/>
            <g fill="#b88a52" fill-opacity="0.28">
              <ellipse cx="10" cy="14" rx="1.1" ry="0.7"/><ellipse cx="18.4" cy="11" rx="0.9" ry="0.6"/>
              <ellipse cx="14" cy="21" rx="1.3" ry="0.8"/><ellipse cx="20.6" cy="25" rx="1" ry="0.6"/>
              <ellipse cx="9.4" cy="28" rx="0.8" ry="0.5"/><ellipse cx="15.6" cy="33" rx="1.1" ry="0.7"/>
              <ellipse cx="21.4" cy="18.4" rx="0.7" ry="0.5"/><ellipse cx="11.6" cy="37" rx="0.9" ry="0.5"/>
            </g>
          </symbol>
          <symbol id="tb-slice-brown" viewBox="0 0 30 46" overflow="visible">
            <path d="M3 46 V16 C-1 12 0 3.4 7.4 2.2 C10.6 -0.6 19.4 -0.6 22.6 2.2 C30 3.4 31 12 27 16 V46 Z" style="fill:var(--toast-deep)"/>
            <path d="M5.6 46 V15.4 C2.6 12.4 3.4 5.6 9 4.8 C11.6 2.6 18.4 2.6 21 4.8 C26.6 5.6 27.4 12.4 24.4 15.4 V46 Z" fill="url(#tb-brown)"/>
            <g fill="#5e3416" fill-opacity="0.32">
              <ellipse cx="10" cy="14" rx="1.1" ry="0.7"/><ellipse cx="18.4" cy="11" rx="0.9" ry="0.6"/>
              <ellipse cx="14" cy="21" rx="1.3" ry="0.8"/><ellipse cx="20.6" cy="25" rx="1" ry="0.6"/>
              <ellipse cx="9.4" cy="28" rx="0.8" ry="0.5"/><ellipse cx="15.6" cy="33" rx="1.1" ry="0.7"/>
            </g>
            <path d="M8 7.4 C12 5.4 18 5.4 22 7.4" fill="none" stroke="#ffe6b8" stroke-opacity="0.45" stroke-width="1.2" stroke-linecap="round"/>
          </symbol>
        </defs>

        <!-- the shadow on the table -->
        <ellipse cx="66" cy="166" rx="56" ry="4.2" fill="#2a1f1a" fill-opacity="0.14"/>

        <!-- the top: chrome, with the two slots -->
        <g data-body>
          <rect x="13" y="70" width="106" height="28" rx="14" fill="url(#tb-top)"/>
          <rect x="13" y="70" width="106" height="28" rx="14" fill="none" stroke="#ffffff" stroke-opacity="0.7" stroke-width="0.8"/>
          <ellipse cx="66" cy="79" rx="46" ry="12" fill="url(#tb-heat)" opacity="0" data-heat/>
          <g>
            <rect x="24" y="74" width="38" height="9.5" rx="4.75" fill="url(#tb-slot)"/>
            <rect x="70" y="74" width="38" height="9.5" rx="4.75" fill="url(#tb-slot)"/>
            <g opacity="0" data-glow>
              <rect x="25.4" y="75.4" width="35.2" height="6.8" rx="3.4" fill="url(#tb-glow)"/>
              <rect x="71.4" y="75.4" width="35.2" height="6.8" rx="3.4" fill="url(#tb-glow)"/>
              <path d="M28 78.6 h30 M74 78.6 h30" style="stroke:var(--glow-hot)" stroke-width="0.9" stroke-linecap="round" stroke-dasharray="3 2.2"/>
            </g>
          </g>

          <!-- the heat over the slots -->
          <g fill="none" stroke-linecap="round" opacity="0" data-shimmer>
            <path d="M36 66 c-3 -5 3 -8 0 -13 s3 -8 0 -12" style="stroke:var(--glow)" stroke-opacity="0.38" stroke-width="1.4"/>
            <path d="M48 64 c-3 -5 3 -8 0 -13 s3 -8 0 -12" style="stroke:var(--glow)" stroke-opacity="0.3" stroke-width="1.2"/>
            <path d="M82 66 c-3 -5 3 -8 0 -13 s3 -8 0 -12" style="stroke:var(--glow)" stroke-opacity="0.38" stroke-width="1.4"/>
            <path d="M95 64 c-3 -5 3 -8 0 -13 s3 -8 0 -12" style="stroke:var(--glow)" stroke-opacity="0.3" stroke-width="1.2"/>
          </g>

          <!-- the bread: behind the front of the body, cut at the slot line so it goes into the slots -->
          <g clip-path="url(#tb-slotline)">
            <g data-slice="0" transform="translate(28 54)">
              <g data-turn>
                <use href="#tb-slice" width="30" height="46"/>
                <use href="#tb-slice-brown" width="30" height="46" opacity="0" data-brown/>
              </g>
            </g>
            <g data-slice="1" transform="translate(74 54)">
              <g data-turn>
                <use href="#tb-slice" width="30" height="46"/>
                <use href="#tb-slice-brown" width="30" height="46" opacity="0" data-brown/>
              </g>
            </g>
          </g>

          <!-- the body: mint enamel, a chrome band with the name, the dial, the lever and the crumb tray -->
          <rect x="10" y="84" width="112" height="76" rx="19" fill="url(#tb-enamel)"/>
          <rect x="10" y="84" width="112" height="76" rx="19" fill="url(#tb-enamel-side)"/>
          <path d="M22 92 C40 87 70 87 96 89" fill="none" stroke="#ffffff" stroke-opacity="0.55" stroke-width="3.2" stroke-linecap="round"/>
          <rect x="10" y="84" width="112" height="76" rx="19" fill="none" style="stroke:var(--mint-deep)" stroke-opacity="0.45" stroke-width="1"/>
          <rect x="10" y="116" width="112" height="11" fill="url(#tb-chrome)"/>
          <path d="M10 116 h112 M10 127 h112" style="stroke:var(--chrome-dark)" stroke-opacity="0.5" stroke-width="0.6"/>
          <text x="58" y="124.2" text-anchor="middle" class="tb__brand" data-brand-text>CRUMB</text>
          <!-- a soft vertical gleam on the enamel, and the chrome skirt at the foot with the crumb tray's pull -->
          <rect x="18" y="90" width="9" height="22" rx="4.5" fill="#ffffff" fill-opacity="0.22"/>
          <rect x="18" y="130" width="9" height="16" rx="4.5" fill="#ffffff" fill-opacity="0.14"/>
          <path d="M10.6 148 h110.8 v1 a11 11 0 0 1 -11 11 h-88.8 a11 11 0 0 1 -11 -11 Z" fill="url(#tb-chrome)"/>
          <path d="M11 148 h110" style="stroke:var(--chrome-dark)" stroke-opacity="0.55" stroke-width="0.6"/>
          <rect x="50" y="151.2" width="32" height="5" rx="2.5" style="fill:var(--ink)" fill-opacity="0.78"/>
          <rect x="54" y="152" width="24" height="1.2" rx="0.6" fill="#ffffff" fill-opacity="0.35"/>

          <!-- the browning dial -->
          <g>
            <circle cx="30" cy="141" r="9.4" style="fill:var(--chrome-dark)" fill-opacity="0.35"/>
            <circle cx="30" cy="140.4" r="8.4" fill="url(#tb-top)" style="stroke:var(--chrome-dark)" stroke-width="0.7"/>
            <g style="stroke:var(--mint-deep)" stroke-width="0.8" stroke-linecap="round">
              <path d="M30 129.6 v1.6 M40.8 140.4 h-1.6 M19.2 140.4 h1.6 M37.6 132.8 l-1.1 1.1 M22.4 132.8 l1.1 1.1"/>
            </g>
            <g data-dial>
              <path d="M30 140.4 L30 133.6" style="stroke:var(--jam)" stroke-width="1.8" stroke-linecap="round"/>
              <circle cx="30" cy="140.4" r="2.2" style="fill:var(--ink)"/>
            </g>
          </g>

          <!-- the lever in its groove on the right -->
          <rect x="104.6" y="93" width="5.8" height="48" rx="2.9" style="fill:var(--ink)" fill-opacity="0.82"/>
          <g data-lever>
            <rect x="98" y="91.6" width="19" height="9.6" rx="4.8" fill="url(#tb-knob)"/>
            <rect x="100.4" y="92.8" width="11" height="2" rx="1" fill="#ffffff" fill-opacity="0.32"/>
          </g>

          <!-- the feet -->
          <rect x="22" y="158" width="15" height="7.4" rx="2.6" style="fill:var(--ink)"/>
          <rect x="95" y="158" width="15" height="7.4" rx="2.6" style="fill:var(--ink)"/>
        </g>

        <!-- steam over the toast, and the crumbs (made by the script) -->
        <g fill="none" stroke-linecap="round" opacity="0" data-steam>
          <path d="M40 44 c-4 -6 4 -10 0 -16 s4 -10 0 -15" stroke="#9fb3b0" stroke-opacity="0.5" stroke-width="1.6"/>
          <path d="M88 42 c-4 -6 4 -10 0 -16 s4 -10 0 -15" stroke="#9fb3b0" stroke-opacity="0.45" stroke-width="1.4"/>
        </g>
        <g data-crumbs></g>
      </svg>

      <p class="sr" aria-live="polite" data-live></p>
    </div>
  </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.

37 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 toaster button” from Animorio and fit it into my site

What it does

On a white background a jam-red pill with the words Order breakfast in cream and Toast & jam in butter yellow. In its round left end stands a retro toaster drawn in SVG: the mint green enamel body with the light on top and the shade on its sides, the chrome top with two dark slots, a chrome band with the name CRUMB, a chrome browning dial with ticks and a red hand, the black bakelite lever in its groove on the right, the slit of the crumb tray and four little feet; two slices of soft bread, with their crust and little holes, stick out of the slots. Under the mouse the lever twitches and the bread rises a hair, as if someone tried it. On a click the lever goes all the way down and the bread sinks into the slots; the toaster settles on its feet, the slots glow orange from the bottom with the coils, the hot air shimmers above them, the dial's hand turns back like a timer and a butter-coloured band fills the pill from the left, while the words turn to Toasting… with a sheen running through them. At the end the lever snaps up: the two slices jump out golden, turn in the air, drop back into the slots and bounce once, a few crumbs fly off, a thread of steam rises and the words become Breakfast's up with a drawn tick. A second click puts fresh bread back in place of the toast.

How it works

The toaster in SVG, in layers

The drawing is a single SVG over the left end of the pill, which takes no pointer. The order of the layers does the trick of the slots: first the chrome top with its dark slots and their glow, then the slices of bread, then the enamel body in front. The slices are cut by a clipPath at the slot line, so they seem to go inside instead of passing in front of the top.

The toast is a second layer

Each slice is two uses of the same drawing: the fresh bread and, over it, the same shape already toasted, with a dark crust and a brown that fades at the edges. The browning is only the opacity of the second layer, which rises up to BROWN while the bread is inside: it shows when the toast jumps out.

Every part on its own pivot

With svgOrigin the body squashes onto its feet, the hand turns around the middle of the dial and each slice turns around its foot, in the SVG's own coordinates: nothing comes loose. The lever only slides up and down inside its groove.

The direction

A GSAP timeline presses the lever and sinks the bread (power3.in), settles the body with elastic.out, lights the glow and the hot air, turns the hand and fills the timer for TOAST_TIME seconds; then it snaps the lever up, throws the slices JUMP pixels up with a few degrees of turn, drops them back with one bounce and throws the crumbs on little arcs. The crumbs use a seeded random, so every pop is the same. SPEED changes the timeScale of it all.

A real button

It is a button: Enter and Space do the same as a click, the keyboard focus has its ring and tries the lever like the mouse, aria-busy and aria-disabled while it toasts, and an aria-live region says Toasting your bread and then Breakfast's up. The second click puts fresh bread back and the button is ready again.

Values to change

NameFileDefaultWhat it changes
LABELscript.jsOrder breakfastThe words at rest.
NOTEscript.jsToast & jamThe small words beside them.
BUSYscript.jsToasting…The words while the bread toasts.
DONEscript.jsBreakfast's upThe words once the toast is out.
BRANDscript.jsCRUMBThe name on the chrome band.
TOAST_TIMEscript.js2.6How long the bread stays in, in seconds.
JUMPscript.js50How high the toast jumps out of the slots, in pixels.
BROWNscript.js0.92How dark the toast comes out.
CRUMBSscript.js10How many crumbs fly off when it pops.
SPEEDscript.js1How fast the whole thing runs.

Try them in the Playground

When to use it

For the order button of a café, a breakfast bar, a bakery or a delivery app, or for a button that starts a job that takes a few seconds.

On phones

The component is 308 pixels wide or the screen minus 32, and 170 tall with a 60 pixel pill; at rest the small words Toast & jam sit under the big ones, so both fit. With no hover, the finger resting on it tries the lever like the mouse, and the tap does not wait for a double tap.

With reduced motion

No lever going down, no bread jumping, no crumbs and no hot air: on a click the bread turns golden in a fade and the pill fills and clears, the words change by fading.

Pitfalls

  • Bread drawn in front of the toaster passed over the chrome top: it has to be drawn between the top and the body, and cut with a clipPath at the slot line.
  • A colour given with a class to the elements inside a symbol does not always reach the copies made with use: the colours of the bread and the toast are written in the style of each path.
  • In SVG an attribute like stroke="var(--x)" does not read CSS variables: it needs style="stroke:var(--x)".
  • Without svgOrigin the slices' turn and the body's squash spun around the SVG's corner and the bread came off the slots.
  • The drawing covers the end of the pill: it needs pointer-events none, or the click would never reach the button.
  • The lever's twitch on hover moved the slices for half a second: if the click came then, the bread stayed up while it toasted. The click now stops that twitch and the bread starts from its place.

Libraries

GSAP 3.15.0, Mona Sans (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 9.0 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