# Toast

A short-lived message that floats above the page: the alert's anatomy, raised, arriving in a corner and leaving on its own after a few seconds unless someone is reading it.

Block: `smbk-toast`. Status: candidate. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `tone` | `default`, `info`, `success`, `warning`, `danger` | `default` | Neutral, or one of the four feedback families on their subtle surface, each with its own icon. |

Parts: `smbk-toast__icon`, `smbk-toast__body`, `smbk-toast__title`, `smbk-toast__description`, `smbk-toast__action`, `smbk-toast__dismiss`, `smbk-toast__region`

## Markup

```html
<div role="status" class="smbk-toast smbk-toast--tone-default">
  <i class="ph ph-bell smbk-icon smbk-icon--size-md smbk-toast__icon" aria-hidden="true"></i>
  <div class="smbk-toast__body">
    <p class="smbk-toast__title">Changes saved</p>
    <p class="smbk-toast__description">Website redesign was updated a moment ago.</p>
  </div>
  <div class="smbk-toast__action">
    <button type="button" class="smbk-btn smbk-btn--tone-secondary smbk-btn--size-sm">
      <span class="smbk-btn__label">Undo</span>
    </button>
  </div>
</div>
```

## React

```tsx
import { Toast } from './design-system/components/toast/toast.js';
import { Button } from './design-system/components/button/button.js';

<Toast tone="default" title="Changes saved" action={<Button tone="secondary" size="sm">Undo</Button>}>Website redesign was updated a moment ago.</Toast>
```

## Stylesheet

```css
/* smbk-toast — Toast. Figma "Toast" 4209:20131. The alert, raised to float above the page.
 * It rises into place as it appears and drops away as it is hidden (motion rules at the end). */

.smbk-toast {
  /* The neutral tone; each feedback tone sets these again below (the tone recipe). */
  --smbk-tone-surface: var(--smbk-color-surface-default);
  --smbk-tone-border: var(--smbk-color-border-rim);
  --smbk-tone-text: var(--smbk-color-text-default);
  --smbk-tone-icon: var(--smbk-color-icon-subtle);
  box-sizing: border-box;
  display: flex;
  align-items: flex-start;
  gap: var(--smbk-space-inline-lg);
  width: var(--smbk-size-content-sm);
  max-width: 100%;
  padding: var(--smbk-space-inset-lg) var(--smbk-space-inset-xl);
  border: var(--smbk-border-width-hairline) solid var(--smbk-tone-border);
  border-radius: var(--smbk-radius-container-sm);
  background: var(--smbk-tone-surface);
  color: var(--smbk-tone-text);
  box-shadow: var(--smbk-elevation-overlay);
}

/* The icon's box is as tall as the title's line, so the icon sits on the first line's middle.
   The title sits on its text with no gap: the line heights give the air. */
.smbk-toast__icon { display: inline-flex; flex-shrink: 0; align-items: center; height: calc(var(--smbk-typography-label-lg-font-size) * var(--smbk-typography-label-lg-line-height)); color: var(--smbk-tone-icon); font-size: var(--smbk-size-icon-md); }
.smbk-toast__body { display: flex; flex: 1 1 0; flex-direction: column; gap: var(--smbk-space-text-label); min-width: 0; }
.smbk-toast__title { margin: 0; font-family: var(--smbk-typography-label-lg-font-family); font-size: var(--smbk-typography-label-lg-font-size); font-weight: var(--smbk-typography-label-lg-font-weight); line-height: var(--smbk-typography-label-lg-line-height); letter-spacing: var(--smbk-typography-label-lg-letter-spacing); text-transform: var(--smbk-typography-label-lg-text-transform); }
.smbk-toast__description { margin: 0; color: var(--smbk-color-text-subtle); font-family: var(--smbk-typography-body-sm-font-family); font-size: var(--smbk-typography-body-sm-font-size); font-weight: var(--smbk-typography-body-sm-font-weight); line-height: var(--smbk-typography-body-sm-line-height); letter-spacing: var(--smbk-typography-body-sm-letter-spacing); }
.smbk-toast__action { display: flex; flex-shrink: 0; flex-wrap: wrap; gap: var(--smbk-space-inline-md); }

/* .smbk-toast__dismiss: the alert's cross, the small press box shared in press-box/press-box.css, with its focus ring
   and its press area on a touch screen. */

/* ---- feedback tones: the tone recipe, each family's subtle surface, its border, its text and its icon ---- */

.smbk-toast--tone-info { --smbk-tone-surface: var(--smbk-color-feedback-info-surface-subtle); --smbk-tone-border: var(--smbk-color-feedback-info-border); --smbk-tone-text: var(--smbk-color-feedback-info-text-on-subtle); --smbk-tone-icon: var(--smbk-color-feedback-info-icon); }
.smbk-toast--tone-success { --smbk-tone-surface: var(--smbk-color-feedback-success-surface-subtle); --smbk-tone-border: var(--smbk-color-feedback-success-border); --smbk-tone-text: var(--smbk-color-feedback-success-text-on-subtle); --smbk-tone-icon: var(--smbk-color-feedback-success-icon); }
.smbk-toast--tone-warning { --smbk-tone-surface: var(--smbk-color-feedback-warning-surface-subtle); --smbk-tone-border: var(--smbk-color-feedback-warning-border); --smbk-tone-text: var(--smbk-color-feedback-warning-text-on-subtle); --smbk-tone-icon: var(--smbk-color-feedback-warning-icon); }
.smbk-toast--tone-danger { --smbk-tone-surface: var(--smbk-color-feedback-danger-surface-subtle); --smbk-tone-border: var(--smbk-color-feedback-danger-border); --smbk-tone-text: var(--smbk-color-feedback-danger-text-on-subtle); --smbk-tone-icon: var(--smbk-color-feedback-danger-icon); }

/* ---- the region: where the page's toasts appear, in the corner at the end of the viewport's
   foot, one under another, the newest last. It is in the page before the first toast arrives,
   with the hidden announcers that say each toast inside it, and takes no room or pointer while it is
   empty. On a phone it spans the viewport less its inset, and so do its toasts. ---- */

.smbk-toast__region {
  position: fixed;
  inset-block-end: var(--smbk-space-inset-xl);
  inset-inline-end: var(--smbk-space-inset-xl);
  z-index: var(--smbk-layer-critical);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--smbk-space-stack-sm);
  max-width: calc(100% - 2 * var(--smbk-space-inset-xl));
  margin: 0;
  padding: 0;
  pointer-events: none;
}

.smbk-toast__region > * {
  pointer-events: auto;
}

@container style(--smbk-layout: narrow) {
  .smbk-toast__region {
    inset-inline-start: var(--smbk-space-inset-xl);
    align-items: stretch;
  }

  .smbk-toast__region > .smbk-toast {
    width: auto;
  }
}

/* ---- motion: a toast fades in and rises a spacing step into place with the enter purpose when
   it appears, and fades out dropping a smaller step with the exit purpose when the page hides it
   with the hidden attribute. Its display changes at the end of the exit, so it stays drawn while
   it leaves; a toast removed from the page outright simply goes. Under reduced motion it fades in and out in
   place, without rising or dropping. ---- */

.smbk-toast {
  transition-property: opacity, translate, display;
  transition-duration: var(--smbk-motion-enter-duration);
  transition-timing-function: var(--smbk-motion-enter-easing);
  transition-behavior: allow-discrete;
}

/* The toast's own display would otherwise outweigh the hidden attribute. */
.smbk-toast[hidden] {
  display: none;
  opacity: 0;
  translate: 0 var(--smbk-space-8);
  transition-duration: var(--smbk-motion-exit-duration);
  transition-timing-function: var(--smbk-motion-exit-easing);
}

@starting-style {
  .smbk-toast {
    opacity: 0;
    translate: 0 var(--smbk-space-16);
  }
}

@media (prefers-reduced-motion: reduce) {
  .smbk-toast {
    transition-property: opacity, display;
  }

  .smbk-toast[hidden] {
    translate: none;
  }
}
```

## Tokens it reads

```json
{
  "size": {
    "target": {
      "min": {
        "$type": "dimension",
        "$value": "{{size.24}}",
        "$description": "Smallest tap area allowed (24, WCAG 2.2)."
      },
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    },
    "icon": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.20}}",
        "$description": "Icon box, medium."
      }
    },
    "content": {
      "sm": {
        "$type": "dimension",
        "$value": "{{size.width.480}}",
        "$description": "Widest a content column may grow, small."
      }
    }
  },
  "typography": {
    "label-lg": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "16px",
        "fontWeight": 500,
        "lineHeight": 1.4,
        "letterSpacing": "0px"
      },
      "$description": "Label LG: labels on controls and short UI text (buttons, chips, tabs, menu headings). Geist Medium, sentence case. Code and keys use Code SM and Code MD."
    },
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  },
  "radius": {
    "full": {
      "$type": "dimension",
      "$value": "{{radius.9999}}",
      "$description": "Capsule: pills, chips, switches."
    },
    "container": {
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.8}}",
        "$description": "Corners of cards, panels and dialogs, small."
      }
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      },
      "offset": {
        "$type": "dimension",
        "$value": "{{space.2}}",
        "$description": "Gap between a control and its focus ring."
      }
    }
  },
  "color": {
    "border": {
      "focus": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Keyboard focus ring on controls."
      },
      "rim": {
        "$type": "color",
        "$value": "{{color.alpha.white.90}}",
        "$description": "The light rim that makes a surface look lit: fields, chosen pills, cards."
      }
    },
    "surface": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.100}}",
        "$description": "Standard surface: cards, panels, the header."
      }
    },
    "text": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Secondary text: descriptions, metadata."
      }
    },
    "icon": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Quiet icons."
      }
    },
    "feedback": {
      "info": {
        "surface-subtle": {
          "$type": "color",
          "$value": "{{color.blue.200}}",
          "$description": "Info messages: pale fill."
        },
        "border": {
          "$type": "color",
          "$value": "{{color.blue.500}}",
          "$description": "Info messages: edge."
        },
        "text-on-subtle": {
          "$type": "color",
          "$value": "{{color.blue.700}}",
          "$description": "Info messages: text on the pale fill."
        },
        "icon": {
          "$type": "color",
          "$value": "{{color.blue.600}}",
          "$description": "The info tone’s own icon (an info sign), so the tone is never shown by colour alone."
        }
      },
      "success": {
        "surface-subtle": {
          "$type": "color",
          "$value": "{{color.green.100}}",
          "$description": "Success messages: pale fill."
        },
        "border": {
          "$type": "color",
          "$value": "{{color.green.500}}",
          "$description": "Success messages: edge."
        },
        "text-on-subtle": {
          "$type": "color",
          "$value": "{{color.green.700}}",
          "$description": "Success messages: text on the pale fill."
        },
        "icon": {
          "$type": "color",
          "$value": "{{color.green.600}}",
          "$description": "The success tone’s own icon (a check), so the tone is never shown by colour alone."
        }
      },
      "warning": {
        "surface-subtle": {
          "$type": "color",
          "$value": "{{color.orange.200}}",
          "$description": "Warning messages: pale fill."
        },
        "border": {
          "$type": "color",
          "$value": "{{color.orange.500}}",
          "$description": "Warning messages: edge."
        },
        "text-on-subtle": {
          "$type": "color",
          "$value": "{{color.orange.700}}",
          "$description": "Warning messages: text on the pale fill."
        },
        "icon": {
          "$type": "color",
          "$value": "{{color.orange.600}}",
          "$description": "The warning tone’s own icon (a warning sign), so the tone is never shown by colour alone."
        }
      },
      "danger": {
        "surface-subtle": {
          "$type": "color",
          "$value": "{{color.red.200}}",
          "$description": "Danger messages: pale fill."
        },
        "border": {
          "$type": "color",
          "$value": "{{color.red.500}}",
          "$description": "Danger messages: edge; the edge of a field in error (3:1)."
        },
        "text-on-subtle": {
          "$type": "color",
          "$value": "{{color.red.700}}",
          "$description": "Danger messages: text on the pale fill."
        },
        "icon": {
          "$type": "color",
          "$value": "{{color.red.600}}",
          "$description": "The danger tone’s own icon (an alert sign), so the tone is never shown by colour alone."
        }
      }
    }
  },
  "space": {
    "8": {
      "$type": "dimension",
      "$value": "8px",
      "$description": "Raw space step, 8 px. Only for fixed distances inside a component; spacing that follows density uses space/inset, stack or inline."
    },
    "16": {
      "$type": "dimension",
      "$value": "16px",
      "$description": "Raw space step, 16 px. Only for fixed distances inside a component; spacing that follows density uses space/inset, stack or inline."
    },
    "inline": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Gap between things side by side, large."
      },
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things side by side, medium."
      }
    },
    "inset": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Padding inside a component, large."
      },
      "xl": {
        "$type": "dimension",
        "$value": "{{space.16}}",
        "$description": "Padding inside a component, extra large."
      }
    },
    "text": {
      "label": {
        "$type": "dimension",
        "$value": "{{space.0}}",
        "$description": "Between a label or title and the line of supporting text right under it, inside a component. None: untrimmed line height already spaces the two lines."
      }
    },
    "stack": {
      "sm": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things stacked one above another, small."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "elevation": {
    "overlay": {
      "$type": "shadow",
      "$value": {
        "light": "0px 12px 16px 2px #373d431a, 0px 2px 8px 0px #373d4326",
        "dark": "0px 12px 16px 2px #00000026, 0px 2px 8px 0px #00000033"
      }
    }
  },
  "layer": {
    "critical": {
      "$type": "number",
      "$value": 1400
    }
  },
  "motion": {
    "enter": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.320}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.expo-out}}"
      }
    },
    "exit": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.200}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.expo-in}}"
      }
    }
  }
}
```

## Purpose

A brief message that floats above the page to confirm something just happened — saved, sent, copied — and then leaves. It has the alert's anatomy and the highest elevation, because it is not part of the page.

## Anatomy

The same parts as the alert: icon, body with title and description, action, dismiss. Each tone has its own icon, as the alert's, and the cross sits in a box big enough to press. The block is raised and has a fixed width.

- The region part, where a page's toasts appear: the corner at the end of the viewport's foot, one under another, the newest last. On a phone it spans the viewport less its inset, and so do its toasts.

## When to use

- Confirming an action whose result is not otherwise visible.
- A brief notice that needs no reply.

## When not to use

- Anything the person must read or act on. Use an [alert](alert.md), or a dialog.
- Errors that block a task. Those belong where the task is.

## Behavior

A toast region shows, times and closes a page's toasts. Put `ToastRegion` round the page once, as it loads, and call `useToasts().show()` anywhere inside it with the toast's tone, title, description and action; it returns an id that `dismiss()` takes.

- A toast stays six seconds, then leaves. A danger toast, and a toast with an action, stay until they are closed. A toast can set its own time, or none.
- While the pointer is on the region or focus is in it, every timer stops, and each starts again with the time it had left once the pointer and focus have gone. A page in a tab nobody is looking at holds them too.
- Escape closes the toast focus is in, and focus moves on to the next toast, or back where it was before it went into the region.
- Three toasts show at once; the others wait their turn and show as earlier ones leave.

A toast fades in and rises a spacing step into place when it appears, and fades out dropping a smaller step as it leaves. Under reduced motion it fades in and out in place, without rising or dropping.

On its own, without a region, the component renders one toast that stays until the page removes it or hides it with the hidden attribute, which plays the same exit. 

## Accessibility
### What it does for you

- Each toast's title and description are announced as it shows, without moving focus: politely, or at once for a danger toast. Two hidden announcers inside the region say them, so none is said twice. A toast in a region has no role of its own; the region is a named landmark.
- A toast on its own carries the status role itself, or the alert role when it is a danger toast.
- F6 moves focus to the newest toast while any shows, so a toast with an action can be reached without tabbing through the page, and F6 again goes back where focus was. The region names the key for screen readers.
- A toast with an action stays until it is closed, unless given a time. A danger toast stays until closed, so it cannot vanish unread.
- While the pointer is on the region or focus is in it, every timer stops, so a toast does not leave while it is being read or reached.
- Escape closes the toast focus is in, and focus moves on to the next toast, or back where it was before it went into the region.

### What you must do

- Put the region in the page as it loads, never with its first toast: it must be there before the first toast arrives, or that toast is not announced.
- Never let a toast be the only place a piece of information appears, because it leaves on its own.
- Give a toast with a time one long enough to reach the action by keyboard.
- Put an error the reader must act on where the task is, not in a toast.
- In plain markup, give the region the status role, `aria-atomic="false"`, so each toast is announced alone, and a name, and give the toasts in it no role; put a danger toast's words in an element with the alert role that is in the page from the start. Timing, holding and Escape are then the page's own script, and must keep to the rules above.
- Give the page's own words for everything it says by itself, such as the dismiss cross's name, which keeps its English until you do; a page in another language can change it.

### How to test

- With a screen reader, show a toast: it is announced without focus moving. Show one more after the page has loaded and check it is announced too.
- Tab into the region: the timers stop, the dismiss cross and any action take focus with a focus ring, and Escape closes the toast.
- Show a toast and press F6: focus moves to it. Press F6 again: focus goes back. Show a danger toast: it is announced at once, interrupting.
- Show a danger toast and one with an action: both stay until closed.
- With reduced motion on, a toast fades in and out in place.

### Keyboard

| Key | What it does |
|---|---|
| F6 | While a toast shows, moves focus to the newest toast's action or dismiss cross; from inside the region, back to where focus was. |
| Tab | Moves focus to a toast's action and its dismiss cross. |
| Enter or Space | Presses the action or the dismiss cross. |
| Escape | Closes the toast focus is in and moves focus to the next toast, or back to where it was before it entered the region. |

## Usage rules

### Do

- Keep it to one line where possible.

### Don't

- Show a toast for something the person did not do, unless it is a status change they are waiting for.
