---
title: Toast
description: Toasts surface brief, non-blocking feedback after an action, such as save confirmations, errors, or background sync status.
---

import { meta as ToastMeta, Default, AlwaysExpanded, Positions } from '@/content/stories/toast';
import Canvas from '@/components/component-docs/canvas';
import Demo from '@/components/component-docs/demo';

export const config = {
	dependencies: {
		components: [Canvas, Demo],
		scripts: [
			'../../../components/component-docs/demo.script.tsx',
			'../../../components/component-docs/canvas.script.tsx',
			'../../../components/component-docs/controls.script.tsx',
		],
	},
};


# Toast

<p class="docs-lede">Toasts surface brief, non-blocking feedback after an action, such as save confirmations, errors, or background sync status.</p>

## Try it

<Demo of={Default} meta={ToastMeta} />

## Usage

Mount `RuiToaster` once at the app root, then call `toast()` imperatively to show messages. Each call returns a toast id you can pass to `toast.dismiss()`.

```tsx
import { RuiToaster, toast } from '@ecopages/radiant-ui/toast';
import { RuiButton } from '@ecopages/radiant-ui/button';

<RuiToaster position="bottom-end" duration={4000} />
<RuiButton on:click={() => toast.success('Changes saved')}>
  Save
</RuiButton>
```

The imperative `toast()` helper is Sonner-shaped: `toast()`, `toast.message()`, `toast.success()`, `toast.error()`, `toast.info()`, `toast.warning()`, `toast.loading()`, `toast.promise()`, `toast.dismiss(id?)`, and `toast.clear()`.

## Match variant to message

Use `success` for confirmations, `error` for failures, `warning` for caution, and `info` for neutral updates. `loading` shows a spinner and never auto-dismisses.

```tsx
toast.success('Changes saved');
toast.error('Unable to reach the server', { description: 'Try again in a moment.' });
toast.warning('Disk space is running low');
toast.info('Your session will expire soon');
```

## Theming

Toast variants reuse the **semantic status roles** from the active theme (the same `info` / `success` / `warning` / `error` families as alerts) via `.rui-toast--*` on the composed surface:

| `variant` | Semantic role | Typical utilities on `.rui-toast--*` |
| --- | --- | --- |
| `info` | `info` | `border-info/40`, `text-info` |
| `success` | `success` | `border-success/40`, `text-success` |
| `warning` | `warning` | `border-warning/40`, `text-warning` |
| `error` | `error` (destructive) | `border-error/40`, `text-error` |
| `loading` | neutral surface | `border-border`, `text-on-surface` |

The surface stays `bg-surface text-on-surface` with a per-variant border and text accent, so toasts read as floating chrome rather than filled status blocks. Set `--info`, `--success`, `--warning`, or `--error` in your app's theme stylesheet to change those accents across the theme.

Toaster geometry uses `--rui-toaster-*` on `rui-toaster`. `gap` and `offset` attributes write `--rui-toaster-gap` and `--rui-toaster-offset` and win over CSS.

| Knob | Default | Notes |
| --- | --- | --- |
| `--rui-toaster-width` | `356px` | CSS-only; not written by JS |
| `--rui-toaster-gap` | `14px` | `gap` attribute wins |
| `--rui-toaster-offset` | `24px` | `offset` attribute wins |
| `--rui-toaster-inset-x` / `--rui-toaster-inset-bottom` | `16px` | used below 600px |

```css
rui-toaster {
  --rui-toaster-width: 28rem;
}
```

## Duration and dismissal

Set `duration` per toast for urgent vs routine messages. Toasts pause their countdown while hovered, while a pointer is interacting, or while the tab is hidden, and resume from the remaining time.

```tsx
toast('Autosaved', { duration: 2000 });
toast.error('Sync failed', { duration: 10_000, closeButton: true });
```

Enable `closeButton` when users need time to read, or `dismissible` to allow swipe-to-dismiss. `toast.promise()` drives a single toast through loading → success/error:

```tsx
toast.promise(save(), {
  loading: 'Saving…',
  success: 'Saved',
  error: 'Save failed',
});
```

## Stacking

Toasts stack at the configured corner. By default the stack collapses behind the front toast (peek) and expands on hover; set `expand` to always show full gaps.

<Canvas of={AlwaysExpanded} meta={ToastMeta} />

```tsx
<RuiToaster expand position="bottom-end" />
```

`visibleToasts` caps how many stay mounted at once; older toasts wait in the queue.

## Position

Place the stack at any corner or edge center.

<Canvas of={Positions} meta={ToastMeta} />

```tsx
<RuiToaster position="top-center" />
```

## Accessibility

- Error toasts use `role="alert"` with `aria-live="assertive"` and `aria-atomic="true"`. The stack has no live attribute, so an alert is never nested inside a polite region.
- Other variants use `role="status"` with `aria-live="off"`. The toaster announces them through a visually hidden `aria-live="polite"` region that sits beside the stack. It clears the region and sets the new text about 100 ms later, so a repeated message ("Saved", "Saved") and a toast updated in place (the same `id`, as `toast.promise()` does) announce again, and toasts shown within that gap are announced together.
- A toast already on screen that turns into an error (a rejected `toast.promise()`) is announced from a second, `aria-live="assertive"` region beside the stack, because switching an existing toast to `role="alert"` is not reliably announced. A new error toast is left to its own alert role.
- Pass `announcement` when the title does not read well on its own; it replaces the title and description in the live region.
- Screen-reader behavior has not been verified yet with NVDA and Firefox, JAWS and Chrome, or VoiceOver and Safari.
- Do not rely on toasts alone for critical errors; pair with inline field errors when appropriate.
- The stack region is focusable (`tabindex="-1"`) so keyboard users can inspect messages; close controls are labeled buttons.

## Why a custom element?

`<rui-toast>` owns dismiss lifetime bookkeeping (pause on hover / hidden tab, swipe-to-dismiss, and exit animation) which requires DOM state. `<rui-toaster>` owns the stack layout (peek/expand math, per-position filtering). `RuiToaster` is the `<rui-toaster>` custom-element class; `RuiToasterElement` is an alias. Mount it once at the app root, then drive it with `toast()`. `RuiToast` remains a JSX helper because it converts numeric `toastId` values to strings; use `RuiToastElement` when you need the `<rui-toast>` class.

## API

### Imperative API

The `toast()` callable and its variants accept `(title, options?)` where `options` is `ToastOptions`:

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string \| number` | auto | Dismiss a specific toast by id, or update it in place. |
| `announcement` | `string` | title and description | Plain text read by the polite live region. Not used for `error` toasts, which announce themselves. |
| `description` | `string` | | Supporting detail under the title. |
| `duration` | `number` | `4000` | Lifetime in ms; `Infinity` keeps it until dismissed. |
| `action` | `{ label, onClick }` | | Render an action button. |
| `dismissible` | `boolean` | `true` | Allow close button / swipe-to-dismiss. |
| `closeButton` | `boolean` | toaster default | Show the corner close control. |
| `closeLabel` | `string` | `Close toast` | Accessible name of the close control. |
| `position` | `ToastPosition` | toaster default | Per-toast placement override. |

### `<rui-toaster>` attributes

| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `position` | `top/bottom-start/center/end` | `bottom-end` | Corner / edge placement. |
| `duration` | `number` | `4000` | Default lifetime in ms. |
| `visible-toasts` | `number` | `3` | Max toasts mounted at once; older wait in queue. |
| `close-button` | `boolean` | `false` | Show a close button on every toast. |
| `expand` | `boolean` | `false` | Always render the stack expanded. |
| `gap` | `number` | `14` | Gap between expanded toasts in px. |
| `offset` | `number` | `24` | Viewport inset in px. |
| `container` | `string` | `''` | CSS selector for a positioning root (docs canvas). |

### Derived tree

`<rui-toaster>` and `<rui-toast>` are **Derived Tree** hosts: the toaster `render()`s `[data-ref="list"]` and `rui-toast` children; each toast `render()`s `[data-ref="toast"]`, `[data-toast-close]`, and `[data-toast-action]`. Use the imperative `toast()` API — do not author these targets by hand. `RuiToaster` and `RuiToast` children are a type error.

The toaster queries `[data-ref="list"]` and `[data-ref="toast"]` for layout math.

### `<rui-toast>` attributes

Rendered by the toaster; listed for completeness. Documented via `@attr` on `RuiToastElement`.

| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `toast-id` | `string` | `''` | Toast id for deadline bookkeeping. |
| `title` | `string` | `''` | Heading text. |
| `description` | `string` | `''` | Supporting detail. |
| `variant` | `default` · `info` · `success` · `warning` · `error` · `loading` | `default` | Status tone. |
| `duration` | `number` | `4000` | Lifetime before auto-dismiss. |
| `dismissible` | `boolean` | `true` | Allow close / swipe. |
| `close-button` | `boolean` | `false` | Corner close control. |
| `close-label` | `string` | `Close toast` | Accessible name of the close control. |
| `action-label` | `string` | `''` | Action button text. |
| `position` | `ToastPosition` | `bottom-end` | Placement; mirrors the toaster. |
| `marked-delete` | `boolean` | `false` | Marks for animated exit. |

### Events

| Event | Detail | Description |
| --- | --- | --- |
| `rui-toast-show` | `ToastShowDetail` | Document-level; the toaster listens and creates a toast. |
| `rui-toast-dismiss` | `{ id? }` | Document-level; the toaster dismisses the matching toast. |
| `rui-toast-mounted` | | Bubbles after a toast mounts and paints; the toaster resyncs stack layout. |

Use `showToast()` / `dismissToast()` to dispatch the document events, or the simpler `toast()` API.

### CSS classes

Public BEM classes on the composed light-DOM surface (documented via `@cssclass` on the elements):

| Class | Description |
| --- | --- |
| `.rui-toaster-region` | Announcement region wrapper (`display: contents`). |
| `.rui-toaster` | The `<ol>` stack list. |
| `.rui-toast` | Toast surface (`role="status"`; `role="alert"` for the `error` variant). |
| `.rui-toast--info` | Info tone. |
| `.rui-toast--success` | Success tone. |
| `.rui-toast--warning` | Warning tone. |
| `.rui-toast--error` | Error tone. |
| `.rui-toast--loading` | Loading tone. |
| `.rui-toast__icon` | Status icon wrapper. |
| `.rui-toast__loader` | Spinner for `loading` toasts. |
| `.rui-toast__content` | Title + description column. |
| `.rui-toast__title` | Heading. |
| `.rui-toast__description` | Supporting detail. |
| `.rui-toast__action` | Optional action button. |
| `.rui-toast__close` | Corner close control. |

### Theme roles (per `variant`)

| Variant | CSS variables consumed |
| --- | --- |
| `info` | `--info` |
| `success` | `--success` |
| `warning` | `--warning` |
| `error` | `--error` |
| `loading` | `--border`, `--on-surface` |

`error` is the destructive status role (same family as destructive buttons). Surface and text roles are `--surface` / `--on-surface` for all variants.
