Toast
A toast reports something that has already happened — a save that went through, a request that did not — without taking the user out of what they were doing. It arrives in a corner, announces itself, and leaves on a timer.
Two pieces make one: Toast.Provider near the root of the app, and useToast() wherever something happens worth mentioning.
Import
import { Toast, useToast } from "@forte-ui/react";Setup
Mount the provider once, above everything that might raise a toast:
import { Toast } from "@forte-ui/react";
export default function App({ children }) {
return (
<Toast.Provider position="bottom-end" timeout={5000} limit={3}>
{children}
</Toast.Provider>
);
}That is the whole setup. The stack comes with the provider rather than being a second thing to remember — Base UI keeps Provider and Viewport apart, which is the right primitive and the wrong default here, because a provider without a viewport fails silently: every toast.success() returns a perfectly good id, the store fills up, and nothing is ever drawn. Pass viewport={false} when you want to place Toast.Viewport yourself.
Toast.Provider renders no DOM element of its own, and the stack is portalled to <body>, so it does not matter where in the tree it sits as long as it is above the components that use the hook.
The hook
useToast() returns one object with a method per kind of news:
const toast = useToast();
toast.success("Profile saved");
toast.error("Could not save", { description: "Check your connection." });
toast.warning("Storage is nearly full");
toast.info("Version 4.2 is now live");
toast.loading("Uploading…");Every one of them takes either a message or a full options object, and the two forms mean the same thing — a bare message becomes the title, which is the field that gets announced:
toast.success("Profile saved");
toast.success({ title: "Profile saved" });
toast.success("Profile saved", { description: "Everyone in the workspace can see it." });toast.show() is the same call with the type left open, for a plain message or an app-specific type of your own:
toast.show("Nothing in particular happened");
toast.show({ title: "Deploy queued", type: "deploy" });type lands on data-type, so a type the library does not know still reaches your CSS ([data-forte="toast"][data-type="deploy"]) — it just gets the neutral glyph and accent.
Every method keeps the same identity for the life of the provider, so one pulled out by destructuring — const { success } = useToast() — is safe in a dependency array. The object itself is rebuilt whenever toasts changes, which is the only way a live list can be live.
Examples
Controlling a toast after it appears
Every method returns a handle rather than a bare hide function, because dismissing is only one of the three things worth doing with a toast you just raised:
const { id, close, update } = toast.loading("Uploading avatar.png");close() dismisses it. update() rewrites it in place — same card, same position in the stack, no second toast — which is what turns "Uploading…" into "Uploaded". id is the toast's identity, generated unless you passed one.
Every field in an update replaces the old one; nothing is merged, data included. So a toast raised with dismissible: false has to say so again if the update should keep it — which the demo below does the other way round, restoring the close button once the upload has finished.
A loading toast never auto-dismisses, whatever timeout says: there is no honest length of time to guess for an operation that has not finished. Resolving it into another type re-arms the timer, so the success card above leaves on its own.
The other way round is options.id. Showing a toast with an id that is already on screen updates that toast and restarts its timer instead of stacking a second copy — which is how a save-status or connection toast stays one card however often the event fires:
toast.info("Reconnecting…", { id: "connection" });Following a promise
toast.promise() is the whole loading → resolved sequence in one call. It returns the promise you gave it, rejection intact, so it is a pass-through rather than a replacement for handling the error.
success and error may each be a message, an options object, or a function of the settled value — which is the form to reach for whenever the message should say what actually happened rather than that something did.
An action button
action is one button inside the toast: { label, ...button props }.
Archived: 0
Clicking it does not close the toast on its own. That is deliberate — an action that can fail has to be able to say so — so close it from the handler once the work is done, as the demo does.
Stacking, and how long a toast stays
Toasts collapse into a deck: the newest in front, the ones behind stepped back and shrunk, their text hidden. Hover it, or move focus into it, and the deck fans out into a readable list.
limit on the provider is how many cards the deck shows, three by default, and it is the only knob for it — collapsed and expanded are the same set of toasts in two arrangements, so there is nothing separate to configure. Raise it to five and five cards stack. The ones past the limit are not thrown away: they are marked data-limited and hidden, and take their turn as the ones in front are dismissed.
timeout is the default life of a toast in milliseconds, and 0 means "until it is closed". A toast can override it per call. Three things pause the countdown, and all three are the same idea — a toast should not expire while nobody is in a position to read it:
- the pointer is over the stack,
- focus is inside it,
- the window is in the background.
That last one matters more than it sounds: without it, every toast raised while the user was in another tab would be gone before they came back.
toast.close(id) closes one; toast.close() closes all.
Position
position on the provider puts the stack in one of six places. start and end are the inline edges, so bottom-end is bottom-right in a left-to-right page and bottom-left in a right-to-left one, with nothing to configure — flip the RTL toggle below and watch it move.
A top stack grows downwards and its cards shrink toward the top edge; a bottom stack does the mirror image. Both are the same three declarations with the sign flipped, through --forte-toast-stack-direction.
Swipe-to-dismiss follows the position too: away from the edge the stack is pinned to, plus both inline directions. Both, rather than only the one matching start / end, because the gesture is physical while the position is logical — accepting either is both simpler and more forgiving. Override it with swipeDirection, or pass [] to turn it off.
Custom contents
renderToast replaces the default layout. Return a Toast.Item — it is what carries the stacking geometry, the swipe gesture and the focus handling — and compose whatever goes inside it from the parts.
Each part still defaults its content from the toast, so a custom arrangement does not mean restating the text: Toast.Title renders the toast's title, Toast.Description its description, Toast.Action its action. Each of them renders nothing when the toast has no such field, which is why the default layout can list all of them unconditionally.
Anything you put in options.data arrives untouched on toast.data. Two keys there are read by Toast.Item itself — icon and dismissible — and both have a shorthand at the top level:
toast.show("Repository starred", { icon: <StarIcon />, dismissible: false });Toast.Content lays its children out in a row with align-items: center. It is flex rather than a fixed grid precisely so a custom render is not bound to four children — give the element that should absorb the free space flex: 1 and min-inline-size: 0.
Outside React
Plenty of the things worth reporting do not happen inside a component: a fetch wrapper, a store, a socket handler. Toast.createManager() gives you the same API as a plain object.
// toaster.ts
import { Toast } from "@forte-ui/react";
export const toaster = Toast.createManager();
// api.ts — no React anywhere
toaster.error("Request failed: 503");Connect it with toastManager on the provider, passing the manager's base.
A manager delivers to a mounted provider and nowhere else — the provider subscribes in an effect, so a call made before that runs, at module load or during the first server render, is dropped rather than queued. Raising toasts from events, which is where they belong, never runs into it.
It has everything useToast() has except toasts — a plain object has nothing to re-render when the list changes, so reading the list there would hand back a snapshot that is already stale. Use the hook when you need the list.
Accessibility
| Key | Behaviour |
|---|---|
| F6 | Moves focus into the toast stack from anywhere on the page, and back out again. |
| Tab | Moves through the buttons inside the focused toast, then on to the next toast. |
| Shift + Tab | Moves backwards; from the stack itself, returns focus to where it was before. |
| Esc | Closes the focused toast. |
The viewport is a polite live region, so a toast is announced when it appears without interrupting whatever the screen reader was saying. Each toast is a dialog labelled by its Toast.Title and described by its Toast.Description — which is why a bare message becomes the title rather than the description.
priority: "high" promotes one toast to an assertive announcement and an alertdialog. Reserve it for something that has to be heard now; a routine confirmation announced assertively cuts off whatever the user was reading.
The four status glyphs are four distinct shapes — a tick, a cross, a triangle, an "i" — and not four colourings of one dot. Colour alone would carry no information for a colour-blind reader, and none at all in forced-colors mode, where every glyph repaints in one system colour (WCAG SC 1.4.1). The text carries the meaning in any case; the glyph is aria-hidden.
Toast.Close is hidden from assistive technology while the deck is collapsed and revealed once the stack is hovered or focused, so it is announced exactly when it is reachable. Moving focus into the stack expands it, which is what makes that work for a keyboard user.
Theming
Every property below is declared on the element it styles: the first six on the viewport, the rest on each toast card. An element's own declaration beats an inherited value, so setting one on :root, on a theme class or on a wrapper has no effect — and the stack is portalled to <body>, so an ancestor of the code that raised the toast could not reach it in any case.
Set the viewport's through className on Toast.Provider (which forwards it to the viewport) and the card's through className on Toast.Item. An unlayered rule beats the library's @layer forte.components whatever its specificity, so a plain class is enough — no !important.
The global tokens these resolve to — --forte-color-*, --forte-radius-*, --forte-space-*, --forte-shadow-* — are the exception: the component only reads those and never redeclares them, so re-pointing them on :root or on a theme scope does re-skin every toast.
--forte-toast-border is the hairline around the card, and it is the one place a toast parts company with the library's other floating surfaces, which rely on a shadow and nothing else. A dialog has a scrim behind it and a select popup visibly grew out of its trigger; a toast arrives unannounced over whatever the page happens to be showing, and --forte-color-overlay is the same value as --forte-color-background. It matters twice over for the deck, whose cards behind the front one show as ~12px strips of exactly that colour — without an edge the stack reads as one card rather than three.
The card stays neutral in all five types, and only the glyph is coloured. Tinting five surfaces would need five new soft tokens and would still leave the type carried by colour alone; the glyph shapes and the text do that job instead. --forte-toast-accent is the one knob to re-point if you want a type to read louder — or set --forte-toast-bg under a [data-type=...] selector of your own.
| Property | Controls | Default |
|---|---|---|
--forte-toast-viewport-inset | Gap between the stack and the edge of the screen | var(--forte-space-4) |
--forte-toast-width | Width of a toast. Shrinks on its own on a screen narrower than this. | 22rem |
--forte-toast-gap | Gap between toasts once the stack is expanded | var(--forte-space-3) |
--forte-toast-peek | How much of each card behind the front one shows | var(--forte-space-3) |
--forte-toast-stack-scale | How much each card behind the front one shrinks, per step | 0.05 |
--forte-toast-z-index | Stacking order of the whole stack | 60 |
--forte-toast-bg | Card background | var(--forte-color-overlay) |
--forte-toast-fg | Card text colour | var(--forte-color-foreground) |
--forte-toast-radius | Corner radius | var(--forte-radius-surface) |
--forte-toast-padding | Padding inside the card | var(--forte-space-4) |
--forte-toast-content-gap | Gap between the icon, the text and the buttons | var(--forte-space-3) |
--forte-toast-shadow | Card shadow | var(--forte-shadow-3) |
--forte-toast-border | Hairline around the card | var(--forte-color-border) |
--forte-toast-accent | Icon colour. data-type re-points it per status. | var(--forte-color-foreground-muted) |
--forte-toast-stack-direction | Which way the stack recedes: -1 for a bottom stack, 1 for a top one | -1 |
--forte-toast-enter-travel | How far the card slides in from, as a share of its own height | 100% |
--forte-toast-enter-duration | Length of the arriving gesture | var(--forte-duration-normal) |
--forte-toast-enter-ease | Curve of the arriving gesture | var(--forte-ease-spring-snappy) |
--forte-toast-exit-duration | Length of the leaving gesture | var(--forte-duration-fast) |
--forte-toast-exit-ease | Curve of the leaving gesture | var(--forte-ease-exit) |
Motion
The stack is four numbers Base UI measures and one CSS rule for each state. --toast-index is how far back a card sits, --toast-offset-y is the total height of everything in front of it, --toast-height is its own, and --toast-frontmost-height is the front card's. Collapsed, a card steps back by --forte-toast-peek and shrinks by --forte-toast-stack-scale; expanded, it moves by the real distance and returns to full size. The transition between the two is a plain interpolation of the same three properties, on --forte-duration-move — the token for positional moves whose distance is only known at runtime.
Which property carries what is load-bearing, and the three are not interchangeable:
translateandscalecarry the stack, as independent properties.transformcarries the arrival, the departure and the swipe, composing on top. Base UI writes an inlinetransformwhile a toast is being dragged and reads it back withgetComputedStyle().transform, which does not see the independent properties — so a drag leaves the stack offsets untouched and simply adds to them.heightclamps every card in the collapsed deck to the front card's height, so a tall toast behind a short one does not stick out and break the illusion of one deck.
Arriving and leaving are the same move in opposite directions — the card slides in from beyond the edge and back out the way it came — so a toast dismissed while it is still arriving reverses instead of snapping. A swiped toast leaves in the direction it was pushed, continuing from wherever the finger let go rather than restarting from zero.
Each expanded card carries a transparent bridge half a gap deep on both block edges, and it is not decoration. Fanned out, the cards are separated by --forte-toast-gap of empty space, and the viewport underneath is pointer-events: none so it cannot swallow clicks meant for the page — which leaves a dead strip between every pair of cards. A pointer crossing one takes the stack with it: the viewport sees a mouseleave, the deck collapses, and it re-expands a frame later when the pointer lands on the next card. Reading down a stack of three made it jump twice. A pseudo-element hit-tests as the element that owns it, so the bridge is the card as far as the pointer is concerned, and consecutive bridges meet in the middle of every gap.
The loading spinner is a descendant of the toast root and not the root itself. Base UI waits on root.getAnimations() before removing a dismissed toast, and an infinite animation's promise never settles, so a spinning root would sit in the DOM for the life of the page. getAnimations() is not called with subtree: true, which is what makes a child safe.
Reduced motion needs no work from you. The only literal geometry here is the enter and exit travel, and it is gated on --forte-motion-ok; the durations shorten on their own, leaving the fade. The collapsed deck's peek and scale are deliberately not collapsed — they are a static arrangement rather than an animation, and flattening them would remove the only sign that more than one toast is waiting.
API reference
useToast()
Returns one object. Every method below is stable between renders.
| Member | Signature | What it does |
|---|---|---|
show | (message, options?) => ToastHandle | Shows a toast of any type. |
success | (message, options?) => ToastHandle | Shows a success toast. |
error | (message, options?) => ToastHandle | Shows an error toast. |
warning | (message, options?) => ToastHandle | Shows a warning toast. |
info | (message, options?) => ToastHandle | Shows an info toast. |
loading | (message, options?) => ToastHandle | Shows a loading toast. Never auto-dismisses. |
promise | (promise, { loading, success, error }) => Promise | Follows a promise. Returns it, rejection intact. |
update | (id, message, options?) => void | Rewrites a toast in place by id. |
close | (id?) => void | Closes one toast, or every toast. |
toasts | ToastObject[] | Every toast in the store, newest first, including the ones leaving. |
message is either a ReactNode — which becomes the title — or a ToastOptions object. When both are given, the object wins field by field.
ToastHandle
| Member | Signature | What it does |
|---|---|---|
id | string | The toast's id. |
close | () => void | Closes this toast. Safe after it has already gone. |
update | (message, options?) => void | Rewrites this toast in place. |
ToastOptions
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | The headline, and the toast's accessible name. |
description | ReactNode | — | The second line, and its accessible description. |
type | "success" | "error" | "warning" | "info" | "loading" | string | — | Which glyph and accent to use. Lands on data-type. |
timeout | number | provider's | Milliseconds before it dismisses itself; 0 never. Ignored for loading. |
priority | "low" | "high" | "low" | "high" announces assertively and renders an alertdialog. |
id | string | generated | Reuse an id to update that toast instead of stacking a second one. |
action | { label: ReactNode } & button props | — | One button inside the toast. |
icon | ReactNode | false | type's glyph | Replaces the glyph; false removes the column. |
dismissible | boolean | true | Whether the close button renders. |
onClose | () => void | — | Called when the toast closes, however it was closed. |
onRemove | () => void | — | Called once it has finished animating out. |
data | object | — | Custom data, reaching renderToast untouched. |
Toast.createManager()
Returns everything useToast() returns except toasts, plus base — the Base UI manager to hand to the provider's toastManager prop.
Toast.Provider
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | The app. | |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
container | HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null | An element to render the stack into instead of `<body>`. Setting it also switches the viewport from `position: fixed` to `position: absolute`, so the stack pins to the container rather than to the screen — give that container `position: relative`. There is no separate prop for that, because a contained viewport that stayed fixed would be a silent bug: the toasts would sit in the screen corner while the container they were scoped to sat somewhere else entirely. The usual reason to reach for it is a preview or an embedded surface that has its own theme scope — a portal to `<body>` escapes the scope, and the toasts come out in the page's colours. | |
limit | number | 3 | How many toasts are visible at once. The rest stay in the store and take their turn as the front ones are dismissed, so nothing is lost — they are marked `data-limited` and hidden rather than dropped. |
position | ToastPosition | bottom-end | Which corner or edge the stack sits at. `start` and `end` are the inline edges, so `bottom-end` is bottom-right in LTR and bottom-left in RTL. |
renderToast | ((toast: ToastObject) => ReactNode) | Renders one toast. Return a `Toast.Item` — it is what carries the stacking geometry and the swipe gesture. ```tsx renderToast={(toast) => ( <Toast.Item toast={toast}> <Toast.Icon /> <Toast.Title /> <Toast.Close /> </Toast.Item> )} ``` Defaults to `<Toast.Item toast={toast} />`, which supplies the standard layout from the toast's own fields. | |
swipeDirection | ToastSwipeDirection | ToastSwipeDirection[] | Which way a toast may be flicked away. Defaults to the direction away from the viewport's own edge plus both inline directions; pass `[]` to turn swipe-to-dismiss off. | |
timeout | number | 5000 | How long a toast stays on screen by default, in milliseconds. `0` keeps every toast until it is closed. A toast can override it with its own `timeout`. |
toastManager | ToastManager<any> | A manager from `Toast.createManager()`, for raising toasts outside React. Pass the manager's `base`. | |
viewport | boolean | true | Whether to render the stack. Turn it off when you are placing `Toast.Viewport` yourself. |
Toast.Provider renders no DOM element of its own; every prop below that is not children, timeout, limit, toastManager or viewport is forwarded to the viewport it renders, along with className and any other <div> prop.
Toast.Viewport
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
container | HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null | An element to render the stack into instead of `<body>`. Setting it also switches the viewport from `position: fixed` to `position: absolute`, so the stack pins to the container rather than to the screen — give that container `position: relative`. There is no separate prop for that, because a contained viewport that stayed fixed would be a silent bug: the toasts would sit in the screen corner while the container they were scoped to sat somewhere else entirely. The usual reason to reach for it is a preview or an embedded surface that has its own theme scope — a portal to `<body>` escapes the scope, and the toasts come out in the page's colours. | |
position | ToastPosition | bottom-end | Which corner or edge the stack sits at. `start` and `end` are the inline edges, so `bottom-end` is bottom-right in LTR and bottom-left in RTL. |
renderToast | ((toast: ToastObject) => ReactNode) | Renders one toast. Return a `Toast.Item` — it is what carries the stacking geometry and the swipe gesture. ```tsx renderToast={(toast) => ( <Toast.Item toast={toast}> <Toast.Icon /> <Toast.Title /> <Toast.Close /> </Toast.Item> )} ``` Defaults to `<Toast.Item toast={toast} />`, which supplies the standard layout from the toast's own fields. | |
swipeDirection | ToastSwipeDirection | ToastSwipeDirection[] | Which way a toast may be flicked away. Defaults to the direction away from the viewport's own edge plus both inline directions; pass `[]` to turn swipe-to-dismiss off. |
Render it yourself only when the stack has to go somewhere specific, and pass viewport={false} to the provider so there is not a second one. It collapses Base UI's Portal → Viewport and the loop over the toasts into one element.
Toast.Item
| Prop | Type | Default | Description |
|---|---|---|---|
toast* | ToastObject | The toast to render — the object a `renderToast` callback receives. | |
children | ReactNode | The toast's contents. Omit it for the standard layout — icon, title, description, action, close — assembled from the toast's own fields. Passing children replaces that layout entirely. Compose it from the parts (`Toast.Icon`, `Toast.Title`, `Toast.Description`, `Toast.Action`, `Toast.Close`), each of which defaults its content from the toast, so a custom arrangement does not mean restating the text. | |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
swipeDirection | ToastSwipeDirection | ToastSwipeDirection[] | Which way this toast may be flicked away. Inherited from the viewport; set it here to override one toast, or pass `[]` to pin it in place. |
Wraps Base UI's Root around its Content. Every Root prop is forwarded — render, id, style and the rest. Omit children for the standard layout.
Toast.Icon
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | The glyph to draw. Defaults to `data.icon` if the toast carries one, and otherwise to the standard glyph for its `type`. | |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Toast.Title
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Toast.Description
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Toast.Action
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Pass render={<Button size="sm" />} to compose it with a styled button; its own quiet styling steps aside when render is present, so the two never fight over the cascade.
Toast.Close
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
Carries aria-label="Close" and a "×" by default; both are overridable.