Toast
A short message that confirms an action or reports a failure, then goes away on its own.
The codebase has two toasts. The app's own screens almost all use sonner, called as toast.success(...). The design system also exports a Toast component, a solid coloured bar, which only the workbook uses.
sonner | Toast from @/ui | |
|---|---|---|
| How you call it | toast.success("…") from anywhere | Render <Toast /> yourself and position it |
Call sites outside _archive | 214 calls in 77 files | 2 renders, both in WorkbookChat |
| Look | White card, grey border, status icon | Solid green, orange or red bar, white text |
| Stacking, queueing, screen reader announcement | Built in | None |
Use sonner in new code. It is what people see everywhere else in the product and it handles placement, stacking and announcements for you.
import { toast } from "sonner";Firing a toast
Call toast.success, toast.error or plain toast after the action finishes. Click the buttons to see the toasts in the bottom-left corner.
import { toast } from "sonner";
<Button label="Publish page" onClick={() => toast.success("Landing page published")} />
<Button label="Sync HubSpot" onClick={() => toast.error("HubSpot sync failed. Try again in a few minutes.")} />
<Button label="Save workflow" onClick={() => toast("Workflow saved as a draft")} />| Call | Calls in the app | Use for |
|---|---|---|
toast.error(message) | 114 | An action failed. Say what failed and what to do. |
toast.success(message) | 87 | An action finished: saved, published, synced, sent. |
toast(message) | 7 | A neutral note with no status. |
toast.info(message) | 3 | A fact that is useful now, such as when data next refreshes. |
toast.warning(message) | 3 | The action ran but something needs a look. |
Info, warning and duration
Toasts close after sonner's default of 4 seconds. Pass duration in milliseconds for a longer message. It is the only option the app passes today.
toast.info("Pipeline data refreshes every 6 hours");
toast.warning(
"No action blocks added. Add a Send email or Publish dashboard block, or save again to continue.",
{ duration: 8000 }
);Where the Toaster lives
toast() only shows something if a <Toaster> is mounted. The app mounts one in src/layouts/RootLayout.jsx, so every screen inside the main layout already has it. Do not add another.
// src/layouts/RootLayout.jsx
import { Toaster } from "sonner";
<Toaster
position="bottom-left"
toastOptions={{
style: {
background: "var(--bg-tertiary)", // #FFFFFF
border: "1px solid var(--border-primary)", // #D4D9EA
color: "var(--text-primary)", // #232532
},
}}
/>Toasts appear in the bottom-left corner. Status comes from sonner's icon only: richColors is off, so every toast has the same white card.
Routes outside RootLayout need their own Toaster. LoginPage mounts a second one at bottom-right with color: "var(--grey-900)", a variable that does not exist, so its text falls back to the inherited colour. This docs site also sits outside the layout; the examples above mount a copy of the RootLayout Toaster.
The Toast component
Toast from @/ui is a solid bar with white 14px medium text, a close button and an optional link. It slides up 16px and fades in over 350ms, and slides down and fades out over 250ms when it closes. Use it only if you are working in the workbook and need to match the existing bars there.
import { Toast } from "@/ui";<Toast status="success" message="Widget added to Q3 pipeline dashboard" onClose={hide} />
<Toast status="warning" message="2 campaigns are missing UTM parameters" onClose={hide} />
<Toast status="error" message="Couldn't publish the landing page" onClose={hide} />status | Background |
|---|---|
success (default) | --color-green #08BD50 |
warning | --color-orange #FBBF24 |
error | --color-red #F93D3D |
There is no info status. White text on these colours is below the 4.5:1 contrast minimum, most of all on warning (about 1.7:1).
With a link
variant="link-right" or "link-left" adds an underlined 12px text button before or after the message.
<Toast
status="success"
variant="link-right"
message="Landing page published"
link="View page"
onLinkClick={openPage}
onClose={hide}
/>
<Toast
status="error"
variant="link-left"
message="HubSpot sync failed"
link="Retry"
onLinkClick={retrySync}
onClose={hide}
/>Auto-dismiss
By default the bar closes itself after 3 seconds and then calls onClose. You own the visibility: render it when something happens and remove it in onClose.
const [shown, setShown] = useState(false);
<Button label="Add to dashboard" onClick={() => setShown(true)} />
{shown && (
<Toast
status="success"
message="Widget added to Q3 pipeline dashboard"
onClose={() => setShown(false)}
/>
)}The component does not position itself. WorkbookChat portals it into document.body inside a wrapper fixed 24px from the bottom and left.
onClose runs 250ms after the exit animation starts. If you do not unmount the toast then, it stays in the layout, invisible, because the animation holds it at opacity 0. The timer restarts whenever onClose changes, so an inline arrow function resets the 3 seconds on every parent re-render; wrap it in useCallback if the parent re-renders often. The message never wraps (white-space: nowrap), so long messages overflow.
Toast props
| Prop | Type | Default | Notes |
|---|---|---|---|
status | "success" | "warning" | "error" | "success" | |
message | ReactNode | "Notification" | |
variant | "default" | "link-right" | "link-left" | "default" | default has no link. |
link | string | "View link" | Link text. Only shown with a link variant. |
onLinkClick | () => void | ||
onClose | () => void | Called after the close button or the timer. | |
duration | number | 3000 | Milliseconds. 0 or less turns auto-dismiss off. |
className | string |
No other prop is passed through.
Accessibility
- sonner renders its toasts in a labelled
aria-liveregion, so screen readers announce them without moving focus. People can pause the timer by hovering over the toasts. Toastfrom@/uihas no role and no live region, so screen readers do not announce it. Its close button hasaria-label="Close"; the link is a native<button>.- Do not put the only way to finish a task inside a toast. It disappears, and keyboard users may never reach it.
Do and don't
- Do use
toast.successandtoast.errorfromsonnerin new code. - Do write what happened in plain words: "Landing page published", "HubSpot sync failed. Try again in a few minutes."
- Do keep it to one line. Put detail on the page, not in the toast.
- Don't paste raw error messages. Many current calls end with
${e.message}; prefer a sentence the person can act on. - Don't toast things the person can already see change on screen, such as a toggle switching.
- Don't mount another
<Toaster>inside the app layout. Every toast would show twice. - Don't use
Toastfrom@/uioutside the workbook.