---
title: Toast
description: Strings, four seconds, bottom corner, announced politely - one provider and one hook, nothing else.
source: https://adamjurek.com/components/toast
---

## Home


A minimal toast system: call `useToast().toast(message)` from anywhere under
`ToastProvider` and a string appears bottom-right for four seconds, announced
via `role="status"`. Reach for it for a plain confirmation or error message -
not for anything with an action button, a promise, or a reason to stay on
screen longer.

<!-- ::start:showcase demo="toast" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

Toasts are kept deliberately small: strings, four seconds, one bottom corner.
The region is `role="status"` so an arrival gets announced politely instead of
interrupting whatever's being read. The whole system is one provider and one
hook on purpose - actions, promises and progress bars belong to the page that
owns that state, not to a passing notification.

> [!NOTE] Install commands are not written here
> Anything in `packages/ui/registry.ts` gets its TanStack and shadcn commands
> attached automatically, so there is nothing to keep in sync.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/toast.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/toast.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | content · Feedback |
| Files | `toast.tsx` |
| Dependencies | None |
| Tags | notification, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { Button, ToastProvider, useToast } from "@sushindustries/ui";

function CopyCommandButton() {
	const { toast } = useToast();

	return (
		<Button onClick={() => toast("Copied the command")}>
			Copy the command
		</Button>
	);
}

export function Example() {
	return (
		<ToastProvider>
			<CopyCommandButton />
		</ToastProvider>
	);
}
```

## What you should see

Nothing, until the button fires. Then a small card appears in the bottom
right corner with the message, stays for four seconds, and disappears -
sliding in with a short animation, gone with none. Fire it twice quickly
and there are two cards stacked, each on its own four-second clock.

## If nothing happens

Calling `useToast()` anywhere that is not inside a `<ToastProvider>` throws
immediately, with a message naming exactly that - so a blank page and a
console error together mean the provider is missing above the component
that called the hook, not that the toast itself failed silently.


## Guides


## Composing it

`ToastProvider` is one instance, mounted once near the root - the same as
`SmoothScroll`. Every `useToast()` call anywhere beneath it shares the one
`role="status"` region and the one bottom-right corner; there is no reason
to mount a second provider except to give a specific subtree its own
`duration`.

## Motion and reduced motion

Each toast animates in with a short slide and fade; under
`prefers-reduced-motion: reduce` that animation is removed and the card
simply appears. Nothing about the four-second dismissal timer changes -
reduced motion affects how it arrives, not how long it stays.

## When not to use it

Strings only, four seconds, one shape - there is no action button, no
promise-tracking, no progress state, and no way to keep one on screen past
its timer. For a message that needs a "Undo" button or has to persist
until someone reads it, `Toast` is the wrong tool; build that with `Dialog`
or `Sheet` instead, where dismissal is deliberate rather than timed.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `children` | `ReactNode` | - |  |
| `duration?` | `number` | `4000` | How long a toast stays, in ms. |

<!-- /generated:api -->

## Notes

`children` here is the app content `ToastProvider` wraps, not a toast
itself - toasts are created only through `useToast().toast(message)`, a
plain string, never JSX. `duration` applies to every toast fired from that
provider; there is no per-call override, so a page that genuinely needs two
different durations needs a second, nested `ToastProvider` around just the
part that does.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="toast" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { Button, useToast } from "@sushindustries/ui";

export function SaveSettingsButton({ onSave }: { onSave: () => Promise<void> }) {
	const { toast } = useToast();

	return (
		<Button
			onClick={async () => {
				await onSave();
				toast("Settings saved");
			}}
		>
			Save
		</Button>
	);
}
```

## What this example is not

`ToastProvider` is not shown here - this component assumes it is already
mounted somewhere above it in the tree, the way it would be once, near the
app's root, not per button.
