Toaster

No dependencies
Code format
React + Tailwind
const css = `
  /* The slices go down, toast while hidden, then pop clear and fall back to rest. */
  @keyframes loader-toaster-slice {
    0% { transform: translateY(0); animation-timing-function: ease-in; }
    10%, 62% { transform: translateY(15px); animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1); }
    70% { transform: translateY(-22px); animation-timing-function: ease-in; }
    80% { transform: translateY(0); animation-timing-function: ease-out; }
    85% { transform: translateY(-4px); animation-timing-function: ease-in; }
    90%, 100% { transform: translateY(0); }
  }
  /* Browned when they come up; swapped for fresh bread while out of sight. */
  @keyframes loader-toaster-brown {
    0%, 11% { fill: light-dark(#6d7179, #7d8087); }
    12% { fill: light-dark(#ebedf0, #e5e6e9); }
    62%, 100% { fill: light-dark(#6d7179, #7d8087); }
  }
  @keyframes loader-toaster-lever { 0% { transform: translateY(0); } 10%, 62% { transform: translateY(17px); } 66%, 100% { transform: translateY(0); } }
  @keyframes loader-toaster-led { 0%, 10% { fill: light-dark(#f3f4f7, #1a1c1f); } 13%, 60% { fill: light-dark(#373a40, #f1f2f3); } 63%, 100% { fill: light-dark(#f3f4f7, #1a1c1f); } }
  @keyframes loader-toaster-heat { 0%, 16% { opacity: 0; transform: translateY(4px); } 30%, 56% { opacity: 0.8; } 62%, 100% { opacity: 0; transform: translateY(-6px); } }
  .loader-toaster { position: relative; width: 120px; height: 115px; }
  .loader-toaster svg { width: 100%; height: 100%; overflow: visible; }
  .loader-toaster__slice { animation: loader-toaster-slice 2.8s infinite; }
  .loader-toaster__slice + .loader-toaster__slice { animation-delay: 0.05s; }
  .loader-toaster__bread { stroke: light-dark(#5f626a, #a7abb2); stroke-width: 1.4; stroke-linejoin: round; animation: loader-toaster-brown 2.8s linear infinite; }
  .loader-toaster__heat { fill: none; stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.4; stroke-linecap: round; opacity: 0; animation: loader-toaster-heat 2.8s ease-in-out infinite; }
  .loader-toaster__body { fill: light-dark(#dbdde1, #2e3035); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.2; }
  .loader-toaster__sheen { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 2; stroke-linecap: round; }
  .loader-toaster__slot { stroke: light-dark(#f3f4f7, #1a1c1f); stroke-width: 2.2; stroke-linecap: round; }
  .loader-toaster__lever { fill: light-dark(#50535b, #c6c8cd); animation: loader-toaster-lever 2.8s cubic-bezier(0.3, 0, 0.3, 1) infinite; }
  .loader-toaster__led { fill: light-dark(#f3f4f7, #1a1c1f); stroke: light-dark(#6d7179, #7d8087); stroke-width: 0.8; animation: loader-toaster-led 2.8s linear infinite; }
  .loader-toaster__dial { fill: light-dark(#cbced3, #393c41); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1; }
  .loader-toaster__foot { fill: light-dark(#cbced3, #393c41); }
  @media (prefers-reduced-motion: reduce) {
    .loader-toaster__slice, .loader-toaster__bread, .loader-toaster__heat, .loader-toaster__lever, .loader-toaster__led { animation: none; }
    .loader-toaster__bread { fill: light-dark(#6d7179, #7d8087); }
  }
`;

// A slice of bread: a puffed crown over a straight body; the body sits down inside the toaster.
const BREAD = 'M3 32V14Q-1 12 0 7Q1 1 8 1Q12 -1 16 1Q23 1 24 7Q25 12 21 14V32Z';

export function ToasterLoader() {
  return (
    <>
      <style>
        {css}
      </style>
      <div className="loader-toaster" role="status" aria-label="Loading">
        <svg viewBox="0 0 100 96" aria-hidden="true">
          <path className="loader-toaster__heat" d="M34 40Q31 34 34 29Q37 24 34 18M50 41Q47 35 50 30Q53 25 50 19M66 40Q63 34 66 29Q69 24 66 18" />
          <g transform="translate(25 34)">
            <g className="loader-toaster__slice">
              <path className="loader-toaster__bread" d={BREAD} />
            </g>
          </g>
          <g transform="translate(51 34)">
            <g className="loader-toaster__slice">
              <path className="loader-toaster__bread" d={BREAD} />
            </g>
          </g>
          <rect className="loader-toaster__body" x="12" y="46" width="76" height="40" rx="14" />
          <path className="loader-toaster__sheen" d="M24 54Q48 51 68 54" />
          <path className="loader-toaster__slot" d="M83 54V74" />
          <rect className="loader-toaster__lever" x="77" y="50" width="12" height="5" rx="2" />
          <circle className="loader-toaster__led" cx="22" cy="64" r="2.4" />
          <circle className="loader-toaster__dial" cx="22" cy="76" r="4" />
          <rect className="loader-toaster__foot" x="20" y="86" width="10" height="4" rx="1.5" />
          <rect className="loader-toaster__foot" x="70" y="86" width="10" height="4" rx="1.5" />
        </svg>
      </div>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/loader-toaster
Source on GitHub

About this component

Two slices sink into a toaster as the lever goes down, the light comes on and heat shimmers over the slots, then the toast pops up browned and drops back to rest.

Dependencies
None
Difficulty
Intermediate
Triggers
continuous, loop
Techniques
keyframes, svg, transform only
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the toast sits still.

More loaders

See all
Wiper
Yo-yo
Ampersand
Beacon