Design system

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.

sonnerToast from @/ui
How you call ittoast.success("…") from anywhereRender <Toast /> yourself and position it
Call sites outside _archive214 calls in 77 files2 renders, both in WorkbookChat
LookWhite card, grey border, status iconSolid green, orange or red bar, white text
Stacking, queueing, screen reader announcementBuilt inNone

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")} />
CallCalls in the appUse for
toast.error(message)114An action failed. Say what failed and what to do.
toast.success(message)87An action finished: saved, published, synced, sent.
toast(message)7A neutral note with no status.
toast.info(message)3A fact that is useful now, such as when data next refreshes.
toast.warning(message)3The 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} />
statusBackground
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).

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

PropTypeDefaultNotes
status"success" | "warning" | "error""success"
messageReactNode"Notification"
variant"default" | "link-right" | "link-left""default"default has no link.
linkstring"View link"Link text. Only shown with a link variant.
onLinkClick() => void
onClose() => voidCalled after the close button or the timer.
durationnumber3000Milliseconds. 0 or less turns auto-dismiss off.
classNamestring

No other prop is passed through.

Accessibility

  • sonner renders its toasts in a labelled aria-live region, so screen readers announce them without moving focus. People can pause the timer by hovering over the toasts.
  • Toast from @/ui has no role and no live region, so screen readers do not announce it. Its close button has aria-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.success and toast.error from sonner in 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 Toast from @/ui outside the workbook.

On this page