# Toast

> A transient notification, fired imperatively from anywhere on the page.

URL: https://apogee.avaui.dev/docs/toast
Last updated: 2026-10-15

A transient notification, fired imperatively from anywhere on the page.

Last updated October 15, 2026

Uses JavaScript

Mounts the toast host (`Toaster`) and defines `window.apogeeToast(message, options)`. Calling it clones a `<template>`, fills in the icon/title/message, and appends it to the host — stacked, auto-dismissing, dismissible by a close button.

**Without JS:** apogeeToast is never defined — nothing fires, and no notification appears. Don't make Toast the only feedback for an action; pair it with a non-JS path (redirect, inline message) where that matters.

Place one `<Toaster />` on the page (commonly in the base layout), then fire toasts from anywhere:

```astro
<Toaster />

<button id="toast-info">Show info</button>
<button id="toast-success">Show success</button>
<button id="toast-warning">Show warning</button>
<button id="toast-danger">Show danger</button>

<script>
document.getElementById('toast-info')?.addEventListener('click', () =>
  apogeeToast('Changes are saved automatically.', { color: 'primary', title: 'Heads up' }))
document.getElementById('toast-success')?.addEventListener('click', () =>
  apogeeToast('Your changes have been saved.', { color: 'positive', title: 'Saved' }))
document.getElementById('toast-warning')?.addEventListener('click', () =>
  apogeeToast('Your session expires in 5 minutes.', { color: 'warning', title: 'Heads up' }))
document.getElementById('toast-danger')?.addEventListener('click', () =>
  apogeeToast('Could not save your changes.', { color: 'critical', title: 'Error' }))
</script>
```

The `<Toaster />` mounted above stays for the rest of this page — every button below fires into that same instance.

## Add

```sh
npx @ava-ui/apogee add toast
```

Bring `@lucide/astro` for the status icons (`Info`, `CircleCheck`, `TriangleAlert`, `CircleAlert`). No other Apogee dependencies. Requires JavaScript — a toast is inherently imperative, so there’s no no-JS fallback markup to fall back to.

## Usage

```astro
---
import Toaster from '@/components/apogee/Toast/Toaster.astro'
---
<!-- once, e.g. in your base layout -->
<Toaster />
```

```js
// anywhere, client-side
apogeeToast('Saved', { color: 'positive', title: 'Done' })
```

Mount **exactly one** `<Toaster />` per page — its region, template, and icon set use fixed element ids, so a second instance won’t get its own toasts.

## Anatomy

`Toast` has two pieces. Individual toasts aren’t slots — the script clones a `<template>` and fills it in per call:

-   **Toaster** — the fixed, positioned host region (`role="region"`) that toasts are appended to.
-   **Toast** — icon (by variant) + optional title + message + close button.

## API

`@composable` — exposed to Content Editors in AVA. Any other attribute passes through to the `Toaster` root.

### `<Toaster>` props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | `'apogee-toaster'` | Host id. Mount one Toaster per page; the script binds to the first. |
| `position` | `'top-start' \| 'top-center' \| 'top-end' \| 'bottom-start' \| 'bottom-center' \| 'bottom-end'` | `'bottom-end'` | Screen corner, in logical terms: `end` is the right in LTR and the left in RTL. |
| `class` | `string` | — | Extra classes on the host, merged last. |

### `apogeeToast(message, options)`

The imperative function `Toaster` defines on `window`, not an Astro prop — call it after the `Toaster` has mounted.

| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `message` | `string` | — | Toast body text. |
| `options.color` | `'primary' \| 'neutral' \| 'positive' \| 'warning' \| 'critical'` | `'primary'` | Icon + border tint. `warning` and `critical` use `role="alert"`; the others use `role="status"`. |
| `options.title` | `string` | — | Bold title line above the message. Omitted entirely if not passed. |
| `options.duration` | `number` (ms) | `4000` | Auto-dismiss delay. `0` makes it sticky (stays until closed). |
| `options.position` | `'top-start' \| 'top-center' \| 'top-end' \| 'bottom-start' \| 'bottom-center' \| 'bottom-end'` | the host’s | Corner for this toast; a region per corner is created on first use. |
| `options.actions` | `{ label: string; onClick?: () => void; href?: string }[]` | — | Small ghost buttons under the message. A click runs `onClick` (or follows `href`), then dismisses the toast. |
| `options.width` | `'short' \| 'long' \| string` | `'short'` (20rem) | Toast width: `long` is 25rem; any CSS length works. |
| `options.featured` | `boolean` | `false` | The colour icon in a FeaturedIcon tile. |
| `options.avatar` | `{ src?: string; name: string; status?: 'online' \| 'offline' }` | — | An avatar in place of the icon; the title line becomes the name. |
| `options.time` | `string` | — | Shown after the title (“Ana Lima · 2 min ago”). |
| `options.progress` | `number` (0–100) | — | A progress bar under the message. The toast stays until progress reaches 100, then its timer starts. |

The timer pauses while the pointer or keyboard focus is on the toast and resumes when it leaves. `apogeeToast()` returns an id; `apogeeToast.dismiss(id)` closes that toast early and `apogeeToast.update(id, { message?, title?, progress? })` changes an open one (a no-op once it is gone).

```astro
<button onclick="apogeeToast('Top start', { position: 'top-start' })">Top start</button>
<button onclick="const id = apogeeToast('Sticky until dismissed', { duration: 0 }); setTimeout(() => apogeeToast.dismiss(id), 3000)">Dismiss after 3s</button>
```

### Actions and width

```astro
<button onclick="apogeeToast('Draft deleted', { title: 'Deleted', width: 'long', duration: 0, actions: [{ label: 'Undo', onClick: () => apogeeToast('Draft restored', { color: 'positive' }) }, { label: 'View trash', href: '#' }] })">Delete with undo</button>
<button onclick="apogeeToast('Short width', { width: 'short' })">Short</button>
<button onclick="apogeeToast('A custom 30rem width for a longer sentence that would otherwise wrap twice.', { width: '30rem' })">30rem</button>
```

Featured icon, an avatar message, and an upload with progress:

```astro
<button onclick="apogeeToast('Your invoice is paid.', { color: 'positive', title: 'Payment received', featured: true })">Featured</button>
<button onclick="apogeeToast('Can you review the brief?', { avatar: { name: 'Ana Lima', status: 'online' }, time: '2 min ago' })">Avatar</button>
<button onclick="let p = 0; const id = apogeeToast('report.pdf', { title: 'Uploading', progress: 0 }); const t = setInterval(() => { p += 20; apogeeToast.update(id, { progress: p, ...(p >= 100 && { title: 'Uploaded' }) }); if (p >= 100) clearInterval(t) }, 400)">Upload</button>
```

## Variants

Every `color`, shown in the hero preview above — click each button to fire `primary`, `positive`, `warning`, and `critical` toasts.

## Positions

All six `position` values (shown as code, not live — only one `Toaster` can be mounted on this page at a time):

```astro
<Toaster position="top-start" />
<Toaster position="top-center" />
<Toaster position="top-end" />
<Toaster position="bottom-start" />
<Toaster position="bottom-center" />
<Toaster position="bottom-end" />
```

## States

-   **Enter** — fades and scales in (`apogee-toast-in`, `--duration-medium` / `--ease-standard`).
-   **Exit** — fades and scales out (`apogee-toast--out`, `--duration-fast` / `--ease-standard`) on close-button click or auto-dismiss.
-   **Sticky** (`duration: 0`) — never auto-dismisses; only the close button removes it.
-   **Reduced motion** — enter/exit animations are disabled under `prefers-reduced-motion: reduce`.

## Examples

A sticky error that needs manual dismissal:

```js
apogeeToast('Payment failed — update your card.', { color: 'critical', title: 'Action needed', duration: 0 })
```

A quick, title-less confirmation:

```js
apogeeToast('Link copied to clipboard.')
```

## Accessibility

| Key | Action |
| --- | --- |
| `Tab` | Reaches the action buttons and the close button; focus pauses the timer. |
| `Enter` `Space` | Press the focused button. |

-   The auto-dismiss timer pauses while the pointer or keyboard focus is on a toast and resumes when it leaves.
    
-   `apogeeToast()` returns an id; `apogeeToast.dismiss(id)` closes that toast early.
    
-   The host is `role="region"` with `aria-label="Notifications"`.
    
-   Each toast is `role="alert"` (`warning` / `danger` — assertive) or `role="status"` (`info` / `success` — polite), so it’s announced without any extra `aria-live` wiring.
    
-   The close button is a real `<button>` with a visible `focus-visible` ring and an `aria-label="Dismiss"` (icon-only).
    
-   **No JS** — `apogeeToast` is never defined, so nothing renders. Never make a toast the only feedback for an action a no-JS user can take.
    
-   Actions are ordinary buttons at the end of the tab order; give a toast with actions `duration: 0` or a long timeout so a keyboard user can reach them.
    

## Tokens

| Token | Used for |
| --- | --- |
| `--border-neutral` / `--background-elevation-raised` / `--foreground-neutral` | Toast border, fill, text |
| `--foreground-neutral-faded` | Message text + close-button icon |
| `--foreground-{hue}` | The icon |
| `--border-{hue}-faded` | The border tint per `color` |
| `--outline-color-focus` | Close-button focus ring |
| `--duration-medium` / `--duration-fast` / `--ease-standard` | Enter / exit animation |

## Do / Don’t

-   **Do** mount exactly one `Toaster` per page, in a layout shared by every route that fires toasts.
-   **Do** use `danger` / `warning` for anything the user must notice; `info` / `success` for routine confirmation.
-   **Don’t** put content the user must act on (a form, a required decision) only in a toast — it auto-dismisses and isn’t reachable after.
-   **Don’t** fire toasts for every minor state change — reserve them for things worth interrupting the user for.

## Content

-   `title` is one or two words (“Saved”, “Error”, “Heads up”).
-   `message` is one short sentence, plain language, states what happened (and, for errors, what to do next).
-   No ending period on `title`; `message` gets one if it’s a full sentence.

## Related

[

AlertAn inline message with a status color and icon — info, success, warning, or critical; inline, filled or as a full-width banner.

](https://apogee.avaui.dev/docs/alert)[

DialogA native <dialog> opened and closed with invoker commands — modal, edge panel, or fullscreen.

](https://apogee.avaui.dev/docs/dialog)[

DismissibleWrap anything with a close button, with an optional auto-dismiss timeout.

](https://apogee.avaui.dev/docs/dismissible)
