# Toast

> A dark floating notice that confirms the result of something the person just did, disappears after a few seconds, and can offer View or Undo.

Source: https://raft-design.botiverse.build/components/toast. Generated from the current release; where it differs from what you remember, this page is right.

A toast is the receipt for something the person just did: a message sent, a setting saved, an agent finishing the job they just gave it. It floats above the page on the dark HUD, says one sentence, and goes away after a few seconds. It can carry one text action, such as View or Undo.

```tsx
import { useCallback, useEffect, useRef, useState } from "react";
import { Check, Copy, Info, SendHorizontal, TriangleAlert } from "lucide-react";
import {
  Button,
  createToastManager,
  ToastAction,
  ToastActions,
  ToastBody,
  ToastClose,
  ToastContent,
  ToastDescription,
  ToastArrow,
  ToastIcon,
  ToastPortal,
  ToastPositioner,
  ToastProvider,
  ToastRoot,
  ToastTitle,
  ToastViewport,
  useToastManager,
} from "@botiverse/raft-ui";
import type { ToastObject, ToastRootProps } from "@botiverse/raft-ui";

const TOP_SWIPE_DIRECTION: ToastRootProps["swipeDirection"] = ["up", "right"];

type ToastDemoData = {
  icon?: "check" | "info" | "send" | "alert" | "copy";
  hasIcon?: boolean;
  hasClose?: boolean;
  contentClassName?: string;
};

export type ToastDemoOptions = {
  mode: "stacked" | "anchored";
  placement: "bottom-center" | "bottom-right" | "top-center" | "top-right";
  side: "top" | "right" | "bottom" | "left";
  align: "start" | "center" | "end";
  icon: boolean;
  description: boolean;
  action: boolean;
  dismissible: boolean;
};

const DEFAULT_TOAST_DEMO_OPTIONS: ToastDemoOptions = {
  mode: "stacked",
  placement: "bottom-center",
  side: "bottom",
  align: "center",
  icon: true,
  description: true,
  action: true,
  dismissible: true,
};

const ICONS = {
  check: Check,
  info: Info,
  send: SendHorizontal,
  alert: TriangleAlert,
  copy: Copy,
} as const;

export function ToastComposition({
  seed = "copy",
  controls = false,
  demoOptions = DEFAULT_TOAST_DEMO_OPTIONS,
  className,
}: {
  seed?: "copy" | "forward" | "clipboard-error";
  controls?: boolean;
  demoOptions?: ToastDemoOptions;
  className?: string;
}) {
  const [portalContainer, setPortalContainer] = useState<HTMLDivElement | null>(null);
  const [toastManager] = useState(() => createToastManager<ToastDemoData>());

  return (
    <div
      ref={setPortalContainer}
      className={["relative h-44 w-full overflow-hidden", className].filter(Boolean).join(" ")}
    >
      <ToastPreviewScene />
      <ToastProvider
        toastManager={toastManager}
        timeout={controls ? 3600 : 0}
        limit={3}
        renderViewport={false}
      >
        {controls ? <ToastDemoControls options={demoOptions} /> : <SeedToast seed={seed} />}
        {controls || portalContainer ? (
          <ToastPortal container={controls ? undefined : portalContainer}>
            <ToastViewport
              strategy={controls ? "fixed" : "absolute"}
              placement={demoOptions.mode === "anchored" ? "none" : demoOptions.placement}
            >
              <ToastList
                placement={demoOptions.mode === "anchored" ? "none" : demoOptions.placement}
              />
            </ToastViewport>
          </ToastPortal>
        ) : null}
      </ToastProvider>
    </div>
  );
}

export function ToastPreviewScene() {
  return (
    <div
      aria-hidden
      className="pointer-events-none absolute inset-0 bg-[oklch(0.955_0.003_286)] dark:bg-[oklch(0.115_0.004_286)]"
    >
      <div className="absolute inset-x-8 top-7 h-10 rounded-lg bg-white/68 shadow-[0_1px_0_oklch(1_0_0/0.75)_inset,0_0_0_1px_oklch(0_0_0/0.025)] dark:bg-white/[0.07] dark:shadow-[0_1px_0_oklch(1_0_0/0.06)_inset,0_0_0_1px_oklch(0_0_0/0.5)]" />
      <div className="absolute left-10 top-10 h-2 w-28 rounded-full bg-foreground/11 dark:bg-white/18" />
      <div className="absolute right-10 top-10 h-2 w-16 rounded-full bg-foreground/8 dark:bg-white/12" />
      <div className="absolute inset-x-8 top-24 grid grid-cols-3 gap-3">
        <div className="h-12 rounded-md bg-white/58 shadow-[0_0_0_1px_oklch(0_0_0/0.02)] dark:bg-white/5.5" />
        <div className="h-12 rounded-md bg-white/45 shadow-[0_0_0_1px_oklch(0_0_0/0.018)] dark:bg-white/4.5" />
        <div className="h-12 rounded-md bg-white/58 shadow-[0_0_0_1px_oklch(0_0_0/0.02)] dark:bg-white/5.5" />
      </div>
    </div>
  );
}

function SeedToast({ seed }: { seed: "copy" | "forward" | "clipboard-error" }) {
  const seededRef = useRef(false);
  const toastManager = useToastManager<ToastDemoData>();

  useEffect(() => {
    if (seededRef.current) return;
    seededRef.current = true;

    if (seed === "copy") {
      toastManager.add({
        id: "copy-links",
        type: "info",
        title: "3 links copied.",
        data: { hasIcon: false, hasClose: false },
      });
      return;
    }

    if (seed === "forward") {
      toastManager.add({
        id: "forwarded",
        type: "info",
        title: "Forwarded to #design-system.",
        description: "The selected messages were sent.",
        data: { icon: "send" },
        actionProps: {
          children: "View",
          onClick: () => toastManager.close("forwarded"),
        },
      });
      return;
    }

    toastManager.add({
      id: "blocked",
      type: "error",
      title: "Clipboard write blocked.",
      description: "Grant clipboard access or copy the links manually.",
      priority: "high",
      data: { icon: "alert" },
      actionProps: {
        children: "Retry",
        onClick: () => toastManager.close("blocked"),
      },
    });
  }, [seed, toastManager]);

  return null;
}

function ToastDemoControls({ options }: { options: ToastDemoOptions }) {
  const toastManager = useToastManager<ToastDemoData>();
  const copyButtonRef = useRef<HTMLButtonElement | null>(null);
  const forwardButtonRef = useRef<HTMLButtonElement | null>(null);
  const errorButtonRef = useRef<HTMLButtonElement | null>(null);

  const getPositionerProps = useCallback(
    (anchor: HTMLButtonElement | null) =>
      options.mode === "anchored" && anchor
        ? {
            anchor,
            side: options.side,
            align: options.align,
            sideOffset: 8,
          }
        : undefined,
    [options.align, options.mode, options.side],
  );

  const handleCopyClick = useCallback(
    () =>
      toastManager.add({
        type: "info",
        title: "Link copied.",
        description: options.description ? "The message link is ready to paste." : undefined,
        positionerProps: getPositionerProps(copyButtonRef.current),
        data: {
          icon: "copy",
          hasIcon: options.icon,
          hasClose: options.dismissible,
        },
        actionProps: options.action
          ? {
              children: "Copy again",
              onClick: () => toastManager.close(),
            }
          : undefined,
      }),
    [getPositionerProps, options, toastManager],
  );

  const handleForwardClick = useCallback(
    () =>
      toastManager.add({
        type: "info",
        title: "Forwarded to #design-system.",
        description: options.description ? "The selected messages were sent." : undefined,
        positionerProps: getPositionerProps(forwardButtonRef.current),
        data: {
          icon: "send",
          hasIcon: options.icon,
          hasClose: options.dismissible,
        },
        actionProps: options.action
          ? {
              children: "View",
              onClick: () => toastManager.close(),
            }
          : undefined,
      }),
    [getPositionerProps, options, toastManager],
  );

  const handleErrorClick = useCallback(
    () =>
      toastManager.add({
        type: "error",
        title: "Clipboard write blocked.",
        description: options.description
          ? "Grant clipboard access or copy the links manually."
          : undefined,
        positionerProps: getPositionerProps(errorButtonRef.current),
        priority: "high",
        data: {
          icon: "alert",
          hasIcon: options.icon,
          hasClose: options.dismissible,
        },
        actionProps: options.action
          ? {
              children: "Retry",
              onClick: () => toastManager.close(),
            }
          : undefined,
      }),
    [getPositionerProps, options, toastManager],
  );

  return (
    <div className="absolute inset-0 z-10 flex flex-wrap items-center justify-center gap-2 px-4">
      <Button
        ref={copyButtonRef}
        type="button"
        size="md"
        variant="primary"
        onClick={handleCopyClick}
      >
        Copy link
      </Button>
      <Button
        ref={forwardButtonRef}
        type="button"
        size="md"
        variant="outline"
        onClick={handleForwardClick}
      >
        Forward
      </Button>
      <Button
        ref={errorButtonRef}
        type="button"
        size="md"
        variant="danger-outline"
        onClick={handleErrorClick}
      >
        Error
      </Button>
    </div>
  );
}

function ToastList({ placement }: { placement: ToastDemoOptions["placement"] | "none" }) {
  const { toasts } = useToastManager<ToastDemoData>();

  return toasts.map((toast) =>
    toast.positionerProps ? (
      <ToastPositioner key={toast.id} toast={toast}>
        <DemoToast toast={toast} layout="inline" placement={placement} showArrow />
      </ToastPositioner>
    ) : (
      <DemoToast key={toast.id} toast={toast} placement={placement} />
    ),
  );
}

function DemoToast({
  toast,
  layout,
  placement,
  showArrow = false,
}: {
  toast: ToastObject<ToastDemoData>;
  layout?: "stacked" | "inline";
  placement: ToastDemoOptions["placement"] | "none";
  showArrow?: boolean;
}) {
  const showDescription = Boolean(toast.description);
  const showAction = Boolean(toast.actionProps?.children);
  const showIcon = toast.data?.hasIcon !== false;
  const showClose = toast.data?.hasClose !== false;

  return (
    <ToastRoot
      toast={toast}
      layout={layout}
      swipeDirection={placement.startsWith("top-") ? TOP_SWIPE_DIRECTION : undefined}
    >
      {showArrow ? <ToastArrow /> : null}
      <ToastContent>
        {showIcon ? (
          <ToastIcon>
            <ToastIconGlyph toast={toast} />
          </ToastIcon>
        ) : null}
        <ToastBody>
          <ToastTitle />
          {showDescription ? <ToastDescription /> : null}
        </ToastBody>
        {showAction || showClose ? (
          <ToastActions>
            {showClose ? <ToastClose /> : null}
            {showAction ? <ToastAction /> : null}
          </ToastActions>
        ) : null}
      </ToastContent>
    </ToastRoot>
  );
}

function ToastIconGlyph({ toast }: { toast: ToastObject<ToastDemoData> }) {
  const Icon = ICONS[toast.data?.icon ?? iconForType(toast.type)];
  return <Icon aria-hidden />;
}

function iconForType(type: string | undefined): keyof typeof ICONS {
  if (type === "success") return "check";
  if (type === "error") return "alert";
  if (type === "warning") return "alert";
  return "info";
}
```

## When to use

| Situation                                  | Use                                                                                                     |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| Something the person just did has a result | Toast                                                                                                   |
| An action that can be reversed in place    | Do it; when a toast offers Undo is in [Feedback](https://raft-design.botiverse.build/patterns/feedback.md#confirming) |
| The state of the whole server              | [NotificationCenter](https://raft-design.botiverse.build/components/notification-center.md)                                     |
| Something that happened elsewhere          | [ActivityInbox](https://raft-design.botiverse.build/components/activity-inbox.md)                                                    |
| Something to confirm or decide             | [Dialog](https://raft-design.botiverse.build/components/dialog.md)                                                                           |
| Something to read before acting            | [Banner](https://raft-design.botiverse.build/components/banner.md)                                                                           |

Never put anything in a toast that would confuse people if they missed it ([Choosing a component](https://raft-design.botiverse.build/components.md#telling-people-something)).

## Anatomy

- `ToastProvider`: holds the toast manager and, by default, renders the viewport. Mount it once near the root.
- `toast`: the command API (`toast.success`, `toast.error`, …) for the default manager.
- `ToastViewport`: where toasts appear on screen. `ToastPositioner` and `ToastArrow` anchor a toast to an element instead.
- `ToastRoot` and `ToastContent`: one toast and its layout.
- `ToastIcon`: optional. Who did it (an avatar), or a status pixel when there is no "who".
- `ToastBody`, holding `ToastTitle` (required) and an optional `ToastDescription`, both in `text-callout` ([Typography](https://raft-design.botiverse.build/foundations/typography.md#type-scale)): 14/20 on a phone, 14/22 on a desktop, like a banner. `ToastIcon` is one line of that text tall, so the icon stays on the title's line: a status light in it is 9px and an icon 16px, centered in a 20px column 12px before the title, as is an avatar ([A mark that leads text](https://raft-design.botiverse.build/foundations/alignment.md#a-mark-that-leads-text)).
- `ToastActions`: optional, holding one `ToastAction` and `ToastClose`.
- `ToastProgress`: an optional line that shows the time left before it closes.

Leave out a part you don't need instead of rendering it empty.

## Variants

The intent sets the pixel in `ToastIcon`. When the toast is about someone, show their avatar there instead.

| Intent                   | Use for                                                         |
| ------------------------ | --------------------------------------------------------------- |
| A person or agent did it | Their avatar in `ToastIcon`: "Bugen finished reviewing #25."    |
| `success`                | Something finished or saved                                     |
| `warning`, `error`       | Something went wrong. `error` is announced at a higher priority |
| `info`                   | A neutral result, such as "3 links copied."                     |
| A message sent as a task | The Todo pixel in `ToastIcon`                                   |

With the `toast` command API, pass the avatar as `leading`. It takes the pixel's place in `ToastIcon`, hidden from assistive technology.

```tsx
import { useCallback, useEffect, useState } from "react";
import {
  Avatar,
  AvatarFallback,
  AvatarImage,
  Button,
  toast,
  ToastProvider,
} from "@botiverse/raft-ui";

/**
 * A toast about someone: their avatar takes the status pixel's place in
 * `ToastIcon`. The command API renders `leading` hidden from assistive
 * technology, so the name stays in the title.
 */
export function PersonContent({ portalContainer }: { portalContainer: HTMLElement | null }) {
  const showReviewDone = useCallback(() => {
    toast.info("Atlas finished reviewing #25.", {
      id: "atlas-review",
      leading: (
        <Avatar size="xs" type="agent">
          <AvatarImage src="/avatars/quoted-preview-atlas.svg" alt="" />
          <AvatarFallback />
        </Avatar>
      ),
      action: { label: "View", onClick: () => toast.dismiss("atlas-review") },
    });
  }, []);

  useEffect(() => {
    showReviewDone();
    return () => toast.dismiss();
  }, [showReviewDone]);

  return (
    <ToastProvider timeout={0} portalContainer={portalContainer} viewportStrategy="absolute">
      <Button type="button" size="md" variant="outline" onClick={showReviewDone}>
        Request review
      </Button>
    </ToastProvider>
  );
}
```

## Behavior

- A toast closes on its own after the provider's `timeout`; hovering or focusing it pauses the timer.
- People can swipe a toast away, or close it with `ToastClose`.
- Stacked toasts appear at the bottom center by default. An anchored toast opens next to the control that caused it.

## Content

- Write one sentence: who, what they did, and the result. Put anything more in `ToastDescription`, in `foreground-on-hud-muted`.
- Label the action View or Undo, or another verb in Title Case.
- Set IDs, counts and times in Space Mono ([Typography](https://raft-design.botiverse.build/foundations/typography.md)).

## Accessibility

Toast wraps Base UI's Toast, which announces each toast in a live region and keeps it open while it has focus.

- Make `ToastTitle` carry the whole meaning. The pixel in `ToastIcon` is hidden from assistive technology, and so is `ToastProgress`.
- Hide an avatar in `ToastIcon` with `aria-hidden`; the name is already in the text. The command API's `leading` does this for you.
- Pass `loadingLabel` to a `ToastAction` that shows a loading state, so its spinner says what it waits for.

## Rules

- Show at most one toast at a time.
- Use `layer-hud` for every toast, with `foreground-on-hud` text. Don't tint a toast with an intent color, and never use yellow: notices are neutral ([Color](https://raft-design.botiverse.build/foundations/color.md#principles)).
- Make the action a bold text action that underlines on hover (`ToastAction`), never a button. Never put a colored button in a toast.
- Give a toast at most one action besides closing it.

## API

### ToastAction

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `loading` | `boolean` | `false` |  |
| `loadingLabel` | `string` | `"Loading"` |  |
| `nativeButton` | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ToastActions

Also takes the attributes of its element.

### ToastArrow

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ToastBody

Also takes the attributes of its element.

### ToastClose

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `nativeButton` | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ToastContent

The content layout (self-start, title-only sizing, body centering) needs to know which parts actually rendered. That used to be an ancestor `:has()` styling descendants — a whole-page recalculation trap in Chromium (#386). It measures the rendered DOM after layout instead, so wrapped parts and parts that render nothing behave exactly like the old selectors did.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ToastDescription

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ToastIcon

Also takes the attributes of its element.

### ToastPortal

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `container` | `HTMLElement \| ShadowRoot \| RefObject<HTMLElement \| ShadowRoot \| null>` |  | A parent element to render the portal element into. |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ToastPositioner

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `Align` | `'center'` | How to align the popup relative to the specified side. |
| `alignOffset` | `number \| OffsetFunction` | `0` | Additional offset along the alignment axis in pixels. Also accepts a function that returns the offset to read the dimensions of the anchor and positioner elements, along with its side and alignment. |
| `anchor` | `Element` |  | An element to position the toast against. |
| `arrowPadding` | `number` | `5` | Minimum distance to maintain between the arrow and the edges of the popup. |
| `collisionAvoidance` | `CollisionAvoidance` |  | Determines how to handle collisions when positioning the popup. |
| `collisionBoundary` | `Boundary` | `'clipping-ancestors'` | An element or a rectangle that delimits the area that the popup is confined to. |
| `collisionPadding` | `Padding` | `5` | Additional space to maintain from the edge of the collision boundary. |
| `disableAnchorTracking` | `boolean` | `false` | Whether to disable the popup from tracking any layout shift of its positioning anchor. |
| `positionMethod` | `"fixed" \| "absolute"` | `'absolute'` | Determines which CSS `position` property to use. |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |
| `side` | `Side` | `'top'` | Which side of the anchor element to align the toast against. May automatically change to avoid collisions. |
| `sideOffset` | `number \| OffsetFunction` | `0` | Distance between the anchor and the popup in pixels. Also accepts a function that returns the distance to read the dimensions of the anchor and positioner elements, along with its side and alignment. |
| `sticky` | `boolean` | `false` | Whether to maintain the popup in the viewport after the anchor element was scrolled out of view. |
| `toast` (required) | `ToastObject<any>` |  | The toast object associated with the positioner. |

Also takes the attributes of its element.

### ToastProgress

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `duration` | `number` | `DEFAULT_TOAST_TIMEOUT_MS` |  |

Also takes the attributes of its element.

### ToastProvider

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `portalContainer` | `HTMLElement \| ShadowRoot \| RefObject<HTMLElement \| ShadowRoot \| null>` |  |  |
| `renderViewport` | `boolean` | `true` |  |
| `viewportClassName` | `string` |  |  |
| `viewportPlacement` | `"none" \| "bottom-right" \| "top-right" \| "bottom-center" \| "top-center"` | `"bottom-center"` |  |
| `viewportStrategy` | `"fixed" \| "absolute"` | `"fixed"` |  |
| `children` | `ReactNode` |  |  |
| `limit` | `number` | `3` | The maximum number of toasts that can be displayed at once. When the limit is exceeded, the oldest toasts are marked as `limited` (via the `data-limited` attribute) rather than removed, so they can be hidden or animated out. |
| `timeout` | `number` | `DEFAULT_TOAST_TIMEOUT_MS` | The default amount of time (in ms) before a toast is auto dismissed. A value of `0` will prevent the toast from being dismissed automatically. |
| `toastManager` | `ToastManager<any>` | `defaultToastManager` | A global manager for toasts to use outside of a React component. |

### ToastRoot

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `layout` | `"inline" \| "stacked"` | `"stacked"` |  |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |
| `swipeDirection` | `"up" \| "down" \| "left" \| "right" \| ("up" \| "down" \| "left" \| "right")[]` | `['down', 'right']` | Direction(s) in which the toast can be swiped to dismiss. |
| `toast` (required) | `ToastRootToastObject<any>` |  | The toast to render. |

Also takes the attributes of its element.

### ToastTitle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ToastViewport

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `placement` | `"none" \| "bottom-right" \| "top-right" \| "bottom-center" \| "top-center"` | `"bottom-center"` |  |
| `strategy` | `"fixed" \| "absolute"` | `"fixed"` |  |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

## Changes

- 0.9.6 (Added): `toast.add/info/success/warning/error(title, { leading })` puts a node, such as a person's avatar, in `ToastIcon` in place of the status pixel. It is `aria-hidden` and still shows with `icon: false`. Calls without `leading` render exactly as before.
- 0.5.35 (Changed): `ToastContent` measures its rendered content and writes `data-has-icon`, `data-has-description` and `data-has-actions` on itself; the content layout keys off these attributes instead of descendant selectors.
