Skip to content

Toast

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/toast.json

The component lands in components/ui/toast.tsx and is yours to edit.

See it liveEvery tone, with a title, a sentence, an action and a dismiss - in either theme, and in the accent of any product of the line.→

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.

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.

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.

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.

PropTypeDefault
toneneutral | 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.

PropTypeDefault
containerHTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | nullWhere 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.

  1. v0.40.0
    • refactor say state on the element, and measure the palette's anchor apart 8d9b4e1
    • fix hold every surface over the page to its room on both axes d0436a7
  2. v0.39.0
    • feat let the frame go on paper, and keep a panel or a chart on one sheet 354d583
  3. v0.26.1
    • fix shade a popup instead of staining the page behind it 9c7a96b
  4. v0.11.0
    • feat Toast, Alert and Banner 8401211