# ToastLoader

> A toaster with a little face crouches and squeezes its eyes shut, then pops two slices of toast up with a sparkle. One slice flips head over heels at the top before both drop back in as the lever goes down. A squash-and-stretch loop of 1.6 seconds.

- Page: https://www.boatui.dev/loaders/toast
- Source: https://github.com/wi11s/boatui/blob/main/components/loaders/toast-loader.tsx
- Animated. Generate: ~1,089 tokens (source size, single pass). Import: ~17 tokens.

## Install

All loaders: `npx degit wi11s/boatui/components/loaders components/loaders`

Only this one: copy the shared files (`loader.tsx`, `loader.module.css`) and `toast-loader.tsx` and `toast-loader.module.css` into `components/loaders/`. Shared files are in https://www.boatui.dev/llms-full.txt.

## Usage

```tsx
import { ToastLoader } from '@/components/loaders/toast-loader';

export function Saving() {
  return <ToastLoader size={48} label="Saving your changes…" />;
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `theme` | `Partial<ToastLoaderTheme>` | — | Colour overrides. Keys listed under theme tokens. |
| `size` | `number` | 64 | Width and height in pixels. |
| `label` | `string` | "Loading…" | Announced to screen readers through role="status". |
| `paused` | `boolean` | false | Freezes the animation. |
| `className` | `string` | — | Applied to the root element (an inline-flex span). |
| `style` | `CSSProperties` | — | Merged into the root element style, after theme variables. |

## Theme tokens

| Key | CSS variable | Default |
| --- | --- | --- |
| `toaster` | `--toaster` | `#9cc9e3` |
| `toasterShade` | `--toaster-shade` | `#7fb2d2` |
| `bread` | `--bread` | `#e9b96e` |
| `crust` | `--crust` | `#b8713a` |
| `spark` | `--spark` | `#f2c14e` |
| `face` | `--face` | `#3d4a57` |

## Source

`components/loaders/toast-loader.tsx`

```tsx
// <ToastLoader>: a toaster that crouches, squeezes its eyes shut, then pops two slices of toast up.
// One slice flips head over heels before both drop back in.

import { LoaderFrame, type LoaderProps } from './loader';
import styles from './toast-loader.module.css';

export const toastLoaderTheme = {
  toaster: '#9cc9e3',
  toasterShade: '#7fb2d2',
  bread: '#e9b96e',
  crust: '#b8713a',
  spark: '#f2c14e',
  face: '#3d4a57',
};
export type ToastLoaderTheme = typeof toastLoaderTheme;

function Slice({ x, className }: { x: number; className: string }) {
  return (
    <g className={className}>
      <path d={`M${x} 56 V36 c0 -7 4 -9 9 -9 c5 0 9 2 9 9 V56 Z`} fill="var(--crust)" />
      <path d={`M${x + 2.5} 56 V37 c0 -5 3 -6.5 6.5 -6.5 c3.5 0 6.5 1.5 6.5 6.5 V56 Z`} fill="var(--bread)" />
    </g>
  );
}

/** A toaster that crouches, then pops two slices of toast up; one flips before they drop back in. */
export function ToastLoader(props: LoaderProps<ToastLoaderTheme>) {
  return (
    <LoaderFrame
      {...props}
      defaultTheme={toastLoaderTheme}
      art={
        <>
          <g fill="none" stroke="var(--spark)" strokeWidth="3" strokeLinecap="round">
            <path className={styles.spark} d="M24 26 l-6 -5 M50 16 v-7 M76 26 l6 -5" />
          </g>
          <Slice x={31} className={styles.slice} />
          <Slice x={51} className={`${styles.slice} ${styles.slice2}`} />
          <g className={styles.toaster}>
            <rect x="20" y="50" width="60" height="36" rx="12" fill="var(--toaster)" />
            <rect x="20" y="78" width="60" height="8" rx="4" fill="var(--toaster-shade)" />
            <rect x="30" y="48" width="18" height="5" rx="2.5" fill="var(--toaster-shade)" />
            <rect x="52" y="48" width="18" height="5" rx="2.5" fill="var(--toaster-shade)" />
            <g className={styles.eyes} fill="var(--face)">
              <ellipse cx="41" cy="66" rx="2.2" ry="2.6" />
              <ellipse cx="59" cy="66" rx="2.2" ry="2.6" />
            </g>
            <path d="M47 71 Q50 73.5 53 71" fill="none" stroke="var(--face)" strokeWidth="1.6" strokeLinecap="round" />
            <ellipse cx="35" cy="70.5" rx="3" ry="1.8" fill="var(--spark)" opacity="0.45" />
            <ellipse cx="65" cy="70.5" rx="3" ry="1.8" fill="var(--spark)" opacity="0.45" />
          </g>
          <g className={styles.lever}>
            <rect x="80" y="58" width="9" height="4" rx="2" fill="var(--toaster-shade)" />
          </g>
          <ellipse cx="50" cy="92" rx="30" ry="3" fill="var(--toaster-shade)" opacity="0.4" />
        </>
      }
    />
  );
}
```

`components/loaders/toast-loader.module.css`

```css
/* One 1.6s loop: the toaster crouches (eyes squeezed shut), the slices pop up, one flips
   head over heels, and both drop back in. */
.toaster {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: squash 1.6s ease-in-out infinite;
}
.slice {
  animation: pop 1.6s cubic-bezier(0.3, 0, 0.3, 1) infinite;
}
.slice2 {
  transform-box: fill-box;
  transform-origin: 50% 50%;
  animation-name: flip;
  animation-delay: 0.06s;
}
.eyes {
  transform-box: fill-box;
  transform-origin: 50% 50%;
  animation: squint 1.6s ease-in-out infinite;
}
.lever { animation: lever 1.6s ease-in-out infinite; }
.spark {
  transform-box: fill-box;
  transform-origin: center;
  opacity: 0;
  animation: spark 1.6s ease-out infinite;
}

@keyframes squash {
  0%, 18%, 100% { transform: scale(1, 1); }
  10% { transform: scale(1.05, 0.93); }
  24% { transform: scale(0.97, 1.04); }
}
@keyframes pop {
  0%, 14% { transform: translateY(0); }
  32% { transform: translateY(-26px); }
  52% { transform: translateY(-20px); }
  72%, 100% { transform: translateY(0); }
}
@keyframes flip {
  0%, 14%   { transform: translateY(0) rotate(0deg); }
  32%       { transform: translateY(-30px) rotate(200deg); }
  50%       { transform: translateY(-22px) rotate(360deg); }
  72%, 100% { transform: translateY(0) rotate(360deg); }
}
@keyframes squint {
  0%, 4%, 22%, 100% { transform: scaleY(1); }
  10%, 16%          { transform: scaleY(0.2); }
}
@keyframes lever {
  0%, 10% { transform: translateY(8px); }
  18%, 70% { transform: translateY(0); }
  90%, 100% { transform: translateY(8px); }
}
@keyframes spark {
  0%, 24% { opacity: 0; transform: scale(0.6); }
  34% { opacity: 1; transform: scale(1); }
  50%, 100% { opacity: 0; transform: scale(1.2); }
}
```
