Toast

Aria-live notification system — Toaster region plus a useToast dispatcher.

organismsSource
import { Toast, Toaster, ToastTitle, ToastDescription, ToastAction, useToast } from '@elirobinson/react/components/organisms/Toast';

Styles: @elirobinson/react/styles/organisms/Toast.css — already included when you import @elirobinson/react/styles.css.

Mount one Toaster near the root of your app, then call useToast() from anywhere inside it to dispatch. Toaster renders nothing visible on its own — it owns the toast queue and portals the viewport to the end of <body>.

Show code
import { Button } from '@elirobinson/react/components/atoms/Button';
import { Toaster, useToast } from '@elirobinson/react/components/organisms/Toast';

function DownloadButton() {
  const { toast } = useToast();

  return (
    <Button
      onClick={() =>
        toast({
          title: 'Guide downloaded',
          description: 'Check your downloads folder for the PDF.',
        })
      }
    >
      Download guide
    </Button>
  );
}

export default function Basic() {
  return (
    <Toaster>
      <DownloadButton />
    </Toaster>
  );
}

When to use it

Use Toast to confirm that something the reader just did worked, or didn't — a save, a send, a payment. It's not for anything the reader needs to act on right now: a toast clears itself after five seconds by default, so it's the wrong place for an error that blocks the next step. Put that in the page instead, where it stays until it's fixed.

Variants

variant covers default, success, warning, danger, and info — pass it straight through toast({ ..., variant }).

Show code
import { Button } from '@elirobinson/react/components/atoms/Button';
import { Toaster, useToast } from '@elirobinson/react/components/organisms/Toast';
import type { ToastVariant } from '@elirobinson/react/components/organisms/Toast';

const VARIANTS: Array<{
  variant: ToastVariant;
  label: string;
  title: string;
  description: string;
}> = [
  {
    variant: 'success',
    label: 'Success',
    title: 'Invoice paid',
    description: 'Thanks — a receipt is on its way to your inbox.',
  },
  {
    variant: 'warning',
    label: 'Warning',
    title: 'Session starts soon',
    description: 'Youth Football Fundamentals begins in 15 minutes.',
  },
  {
    variant: 'danger',
    label: 'Danger',
    title: 'Payment failed',
    description: 'Your card was declined — try again or use a different card.',
  },
  {
    variant: 'info',
    label: 'Info',
    title: 'Guide updated',
    description: 'Basketball Conditioning now includes two new drills.',
  },
];

function VariantButtons() {
  const { toast } = useToast();

  return (
    <div className="demo-row">
      {VARIANTS.map(({ variant, label, title, description }) => (
        <Button
          key={variant}
          variant="secondary"
          onClick={() => toast({ title, description, variant })}
        >
          {label}
        </Button>
      ))}
    </div>
  );
}

export default function Variants() {
  return (
    <Toaster>
      <VariantButtons />
    </Toaster>
  );
}

Composing a toast with an action

toast() only takes title, description, variant, and duration — there's no slot for a custom action button. When you need one (an "Undo", a link to the affected record), compose Toast, ToastTitle, ToastDescription, and ToastAction directly instead of going through useToast(). Composed this way it renders in place, not through the portal, so it shows up wherever you put it — here, inline in the demo stage.

Guide removed from cart
Basketball Conditioning was taken out of your order.
Show code
import { useState } from 'react';
import {
  Toast,
  ToastAction,
  ToastDescription,
  ToastTitle,
} from '@elirobinson/react/components/organisms/Toast';

export default function WithAction() {
  const [dismissed, setDismissed] = useState(false);

  if (dismissed) {
    return <p className="t-body">Dismissed — refresh the page to bring it back.</p>;
  }

  return (
    <Toast onDismiss={() => setDismissed(true)}>
      <ToastTitle>Guide removed from cart</ToastTitle>
      <ToastDescription>Basketball Conditioning was taken out of your order.</ToastDescription>
      <ToastAction onClick={() => setDismissed(true)}>Undo</ToastAction>
    </Toast>
  );
}

Props

PropTypeDefaultDescription
onDismiss(() => void)
variant"default" | "success" | "warning" | "danger" | "info"default

Also accepts all HTMLAttributes<HTMLDivElement> props.

Toaster

No props of its own beyond the inherited HTML attributes.

ToastTitle

No props of its own beyond the inherited HTML attributes.

Also accepts all HTMLAttributes<HTMLDivElement> props.

ToastDescription

No props of its own beyond the inherited HTML attributes.

Also accepts all HTMLAttributes<HTMLDivElement> props.

ToastAction

No props of its own beyond the inherited HTML attributes.

Also accepts all HTMLAttributes<HTMLButtonElement> props.

Accessibility

  • Toaster's viewport carries aria-live="polite" and aria-relevant="additions", so a screen reader announces each new toast without interrupting whatever the reader is doing.
  • Each Toast also has role="status" — a live region in its own right, harmless alongside the viewport's aria-live.
  • The dismiss button gets a built-in aria-label="Dismiss notification" automatically; you don't supply one.
  • Toasts auto-dismiss after duration (5000ms by default). Pass duration: 0 to keep one on screen until the reader, or your code, dismisses it.
  • The viewport is portaled to document.body and fixed to the bottom-right corner — it renders outside wherever in the DOM you called toast() from, including outside the demo stage above.
  • Toaster skips its portal on the server and first client render (via the package's useHasMounted hook), so it server-renders cleanly in Next.js and Remix — no client-only boundary needed. Toasts only exist after an interaction, so nothing is lost.
  • Nothing moves focus when a toast appears. A reader mid-task is never interrupted; a ToastAction, when present, is just a normal button already in the page's tab order.

Do

  • Mount exactly one Toaster near the root of the app, and call useToast() from any descendant.
  • Keep title short and put detail in description — they render as a heading and a supporting line, not one paragraph.
  • Set variant to match what actually happened instead of leaving every toast at the default.
  • Pass duration: 0 for a toast that reports something the reader must notice, like a failed payment.

Don't

  • Call useToast() outside a Toaster — it throws on purpose rather than silently doing nothing.
  • Use a toast for something the reader has to act on immediately — it disappears on its own by default.
  • Expect an action button to work through toast({...}) — the dispatcher's data shape has no slot for one; compose Toast directly instead.
  • Mount more than one Toaster in the same app — one queue and one viewport is the whole model.