Toast
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/toast.jsonThe component lands in components/ui/toast.tsx and is yours to edit.
When this and not an Alert, a Banner or a Dialog
Section titled “When this and not an Alert, a Banner or a Dialog”The three messages look similar and mean different things, so the choice is about what the message is, not how much room it needs.
Reach for Toast when something already happened, needs no decision from the reader, and stops being interesting almost immediately: saved, sent, copied, deleted with an undo. If dismissing it would lose information, it is not a toast — a toast is guaranteed to go away on its own, and anything the reader might need to come back to has gone with it.
Reach for Alert when the message is a condition that is still true — and will still be true after a reload — about the thing it sits beside: this field could not be saved, this profile has no axes yet.
Reach for Banner when the condition is about the whole application rather than one part of it, and is true on every screen: you are offline, this build is a preview.
Reach for Dialog when the reader has to answer something. A toast that asks a question is a question the reader can miss by looking away.
Why Base UI’s toast and not Sonner
Section titled “Why Base UI’s toast and not Sonner”Sonner is the usual answer, and it is a good library. It is not the right one
here: @base-ui/react is already a dependency of half of these components, and
its toast brings a manager that does more than toast() — add, update,
close, promise, priorities, a queue with a limit, pause on hover and focus,
and swipe to dismiss. A product installing dowel would be installing a second
toast library to get less.
So what is in this file is the clothes and the tone vocabulary. The queue, the live region, the timers and the gestures are Base UI’s.
Three pieces: a provider around the part of the application that can raise one, a viewport where they stack, and the manager wherever something happens.
<ToastProvider> <App /> <ToastViewport> <Toasts /> </ToastViewport></ToastProvider>function Toasts() { const { toasts } = useToastManager() return toasts.map((toast) => ( <Toast key={toast.id} toast={toast}> <ToastTitle /> <ToastDescription /> <ToastClose aria-label={t('dismiss')} /> </Toast> ))}const manager = useToastManager()
manager.add({ type: 'success', title: t('saved') })
manager.add({ type: 'info', title: t('deleted'), description: t('deletedWhat', { name }), actionProps: { onClick: undo },})
manager.promise(save(), { loading: t('saving'), success: t('saved'), error: t('couldNotSave'),})One provider per screen. Nesting them gives a product two queues that do not know about each other, and a toast raised into the wrong one is a toast nobody sees.
To raise a toast from outside React — a store, an event handler, a worker —
build the manager with createToastManager() and pass it to ToastProvider’s
toastManager prop.
Tone follows the type
Section titled “Tone follows the type”type is Base UI’s word for what happened; tone is dowel’s word for how it
looks. The component maps one onto the other, so a product that says what
happened does not also have to say which colour that is:
type |
tone |
|---|---|
success |
good |
warning |
warn |
error |
bad |
info, loading |
info |
| anything else | neutral |
Passing tone explicitly wins over the map, for the case where a product
knows better.
Colour is emphasis, never the message. The stripe down the side says the same thing the sentence says, so a reader who does not separate green from red gets the whole of it.
Accessibility
Section titled “Accessibility”Toast is a role="dialog" inside the viewport’s role="region", which is
what makes it announced without stealing focus. Base UI drives the live region,
the timers, the pause on hover and focus, and the swipe.
ToastClose needs a label — it draws a cross and nothing else, and a bare
cross is announced as nothing. It is deliberately aria-hidden while the
stack is collapsed and neither hovered nor focused: a collapsed stack is one
announcement, not one announcement plus a dismiss button per toast.
Props
Read from the types in ui/toast.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.
ToastProvider
Wraps the part of the application that can raise a toast. One per screen; nesting them gives a product two queues that do not know about each other.
Base UI's Toast.Provider, unchanged under dowel's name - its props are on its own page.
ToastTitle
The heading. Base UI points the toast's aria-labelledby at it.
Every prop of Base UI's Toast.Title, passed on.
ToastDescription
The sentence under it, and the toast's aria-describedby. The part that scrolls when the sentence is longer than the window.
Every prop of Base UI's Toast.Description, passed on.
ToastAction
The one thing the reader can do about it: undo, or go and look.
Every prop of Base UI's Toast.Action, passed on.
ToastClose
The dismiss button. Needs a word - a bare cross is announced as nothing.
Every prop of Base UI's Toast.Close, passed on.
Toast
One toast. The tone comes from the caller or from toast.type, so a product that raises them through the manager gets the stripe for free.
| Prop | Type | Default | |
|---|---|---|---|
tone | neutral | good | warn | bad | info | 'neutral' |
And every prop of Base UI's Toast.Root, passed on.
ToastViewport
Where they stack. Bottom right by default, which is the corner that does not cover a form being filled in or a menu being read.
| Prop | Type | Default | |
|---|---|---|---|
container | HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null | Where to portal to. Defaults to the document body, which is what keeps toasts above everything regardless of where they were raised from. |
And every prop of Base UI's Toast.Viewport, passed on.
Accessibility
The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 6 rules applied and passed, and none failed. The accessibility report has every item.
History
Each release that changed ui/toast.tsx - the file a product copies - newest first.