# Toast

A succinct message that is displayed temporarily.

```tsx
import { Button } from "@/components/ui/button"
import { toast } from "@/components/ui/toast"

export default function ToastDemo() {
  function showToast() {
    const id = toast.add({
      title: "Event created",
      description: "Sunday, December 3 at 9:00 AM",
      actionProps: {
        children: "Undo",
        onClick() {
          toast.close(id)
        },
      },
    })
  }

  return (
    <Button variant="outline" onClick={showToast}>
      Show toast
    </Button>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx lucide-react
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` and `state-styles.ts`
   from the same page. `states.ts` holds the opaque popup surface a toast is built
   on, and `state-styles.ts` is how a toast and its content read the stack's state.
   The toast also needs the [Button](https://carbon.style/docs/components/button), which it uses for
   the action and the close control, the [Spinner](https://carbon.style/docs/components/spinner) for a
   `loading` toast's glyph, and the [Stack](https://carbon.style/docs/components/stack) for its text
   column.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/toast.tsx"
   "use client"

   import type { CSSProperties, ReactNode } from "react"
   import { Toast as ToastPrimitive } from "@base-ui/react/toast"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"
   import { CircleCheck, Info, OctagonX, TriangleAlert, X } from "lucide-react"

   import { Button } from "@/components/ui/button"
   import { Spinner } from "@/components/ui/spinner"
   import { VStack } from "@/components/ui/stack"
   import { stateStyles } from "@/lib/state-styles"
   import { breakpoints } from "@/theme/breakpoints.stylex"
   import { states } from "@/theme/states"
   import {
     color,
     duration,
     ease,
     layer,
     leading,
     radius,
     shadow,
     space,
     text,
   } from "@/theme/tokens.stylex"

   const toast = ToastPrimitive.createToastManager()

   const styles = stylex.create({
     viewport: {
       position: "fixed",
       zIndex: layer.popup,
       bottom: space.s4,
       right: space.s4,
       left: { default: space.s4, [breakpoints.sm]: "auto" },
       width: { default: "auto", [breakpoints.sm]: "100%" },
       marginInline: { default: "auto", [breakpoints.sm]: 0 },
       maxWidth: "24rem",
       pointerEvents: "none",
       outlineStyle: "none",
     },
     stackOrder: {
       zIndex: "calc(1000 - var(--toast-index))",
     },
     root: {
       position: "absolute",
       right: 0,
       bottom: 0,
       width: "100%",
       height: "var(--carbon-toast-height)",
       transformOrigin: "bottom",
       borderRadius: radius.xl,
       boxShadow: {
         default: shadow.popupLifted,
         ":focus-visible": `0 0 0 1px ${color.ring}, 0 0 0 3px color-mix(in oklab, ${color.ring} 30%, transparent), ${shadow.popupLifted}`,
       },
       userSelect: "none",
       pointerEvents: "auto",
       willChange: "transform",
       "--carbon-toast-gap": space.s3,
       "--carbon-toast-peek": space.s3,
       "--carbon-toast-height": "var(--toast-frontmost-height, var(--toast-height))",
       "--carbon-toast-scale": "max(0, 1 - var(--toast-index) * 0.1)",
       "--carbon-toast-shrink": "calc(1 - var(--carbon-toast-scale))",
       "--carbon-toast-offset-y":
         "calc(-1 * var(--toast-offset-y) - var(--toast-index) * var(--carbon-toast-gap) + var(--toast-swipe-movement-y))",
       transform:
         "translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) - var(--toast-index) * var(--carbon-toast-peek) - var(--carbon-toast-shrink) * var(--carbon-toast-height))) scale(var(--carbon-toast-scale))",
       transitionProperty: "transform, opacity, height",
       transitionDuration: {
         default: `${duration.stack}, ${duration.stack}, 150ms`,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: `${ease.decelerate}, ${ease.standard}, ${ease.standard}`,
       "::after": {
         content: '""',
         position: "absolute",
         top: "100%",
         left: 0,
         width: "100%",
         height: "calc(var(--carbon-toast-gap) + 1px)",
       },
     },
     expanded: {
       height: "var(--toast-height)",
       transform: "translateX(var(--toast-swipe-movement-x)) translateY(var(--carbon-toast-offset-y))",
     },
     limited: { opacity: 0 },
     starting: { transform: "translateY(150%)" },
     content: {
       display: "flex",
       height: "100%",
       alignItems: "center",
       gap: space.s3,
       overflow: "hidden",
       padding: space.s4,
       transitionProperty: "opacity",
       transitionDuration: {
         default: duration.stackFade,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: ease.decelerate,
     },
     behind: { opacity: 0 },
     text: { minWidth: 0 },
     title: { fontSize: text.sm, lineHeight: leading.sm, fontWeight: 500 },
     description: { fontSize: text.sm, lineHeight: leading.sm, color: color.mutedForeground },
     action: { flexShrink: 0 },
     close: {
       flexShrink: 0,
       color: { default: color.mutedForeground, ":hover": color.foreground },
     },
     icon: { display: "flex", flexShrink: 0, "--carbon-icon-size": space.s4 },
     iconError: { color: color.destructive },
   })

   const exits = stylex.create({
     none: { transform: "translateY(150%)" },
     down: { transform: "translateY(calc(var(--toast-swipe-movement-y) + 150%))" },
     up: { transform: "translateY(calc(var(--toast-swipe-movement-y) - 150%))" },
     left: {
       transform:
         "translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--carbon-toast-offset-y))",
     },
     right: {
       transform:
         "translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--carbon-toast-offset-y))",
     },
   })

   type Overrides = { xstyle?: StyleXStyles; className?: string }

   type StateOverrides = Overrides & { style?: CSSProperties }

   function ToastProvider(props: ToastPrimitive.Provider.Props) {
     return <ToastPrimitive.Provider {...props} />
   }

   function ToastPortal(props: ToastPrimitive.Portal.Props) {
     return <ToastPrimitive.Portal data-slot="toast-portal" {...props} />
   }

   type ToastViewportProps = Omit<ToastPrimitive.Viewport.Props, "className"> & Overrides

   function ToastViewport({ xstyle, className, ...props }: ToastViewportProps) {
     const sx = stylex.props(styles.viewport, xstyle)
     return (
       <ToastPrimitive.Viewport
         data-slot="toast-viewport"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type ToastProps = Omit<ToastPrimitive.Root.Props, "className" | "style"> & StateOverrides

   function Toast({ xstyle, style, className, ...props }: ToastProps) {
     return (
       <ToastPrimitive.Root
         data-slot="toast"
         {...props}
         {...stateStyles(
           (state) => [
             states.popupSurface,
             styles.root,
             styles.stackOrder,
             state.expanded && styles.expanded,
             state.transitionStatus === "starting" && styles.starting,
             state.transitionStatus === "ending" &&
               !state.limited &&
               exits[state.swipeDirection ?? "none"],
             state.limited && styles.limited,
             xstyle,
           ],
           { className, style },
         )}
       />
     )
   }

   type ToastContentProps = Omit<ToastPrimitive.Content.Props, "className" | "style"> & StateOverrides

   function ToastContent({ xstyle, style, className, ...props }: ToastContentProps) {
     return (
       <ToastPrimitive.Content
         data-slot="toast-content"
         {...props}
         {...stateStyles(
           (state) => [styles.content, state.behind && !state.expanded && styles.behind, xstyle],
           { className, style },
         )}
       />
     )
   }

   type ToastTitleProps = Omit<ToastPrimitive.Title.Props, "className"> & Overrides

   function ToastTitle({ xstyle, className, ...props }: ToastTitleProps) {
     const sx = stylex.props(styles.title, xstyle)
     return (
       <ToastPrimitive.Title
         data-slot="toast-title"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type ToastDescriptionProps = Omit<ToastPrimitive.Description.Props, "className"> & Overrides

   function ToastDescription({ xstyle, className, ...props }: ToastDescriptionProps) {
     const sx = stylex.props(styles.description, xstyle)
     return (
       <ToastPrimitive.Description
         data-slot="toast-description"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   function ToastAction({
     render = <Button variant="secondary" size="sm" xstyle={styles.action} />,
     ...props
   }: ToastPrimitive.Action.Props) {
     return <ToastPrimitive.Action data-slot="toast-action" render={render} {...props} />
   }

   function ToastClose({
     render = <Button variant="ghost" size="icon-sm" xstyle={styles.close} />,
     children,
     ...props
   }: ToastPrimitive.Close.Props) {
     return (
       <ToastPrimitive.Close data-slot="toast-close" aria-label="Close" render={render} {...props}>
         {children ?? <X aria-hidden="true" />}
       </ToastPrimitive.Close>
     )
   }

   const icons: Record<string, ReactNode> = {
     success: <CircleCheck aria-hidden="true" />,
     info: <Info aria-hidden="true" />,
     warning: <TriangleAlert aria-hidden="true" />,
     error: <OctagonX aria-hidden="true" {...stylex.props(styles.iconError)} />,
     loading: <Spinner aria-hidden="true" />,
   }

   function ToastIcon({ type }: { type: string | undefined }) {
     const icon = type ? icons[type] : null
     if (!icon) return null

     const sx = stylex.props(styles.icon)
     return (
       <span data-slot="toast-icon" {...sx} className={clsx("carbon-icons", sx.className)}>
         {icon}
       </span>
     )
   }

   function ToastList() {
     const { toasts } = ToastPrimitive.useToastManager()

     return toasts.map((item) => (
       <Toast key={item.id} toast={item}>
         <ToastContent>
           <ToastIcon type={item.type} />
           <VStack gap={1} grow xstyle={styles.text}>
             <ToastTitle />
             <ToastDescription />
           </VStack>
           <ToastAction />
           <ToastClose />
         </ToastContent>
       </Toast>
     ))
   }

   function Toaster({ children, toastManager = toast, ...props }: ToastPrimitive.Provider.Props) {
     return (
       <ToastProvider toastManager={toastManager} {...props}>
         {children}
         <ToastPortal>
           <ToastViewport>
             <ToastList />
           </ToastViewport>
         </ToastPortal>
       </ToastProvider>
     )
   }

   const createToastManager = ToastPrimitive.createToastManager
   const useToastManager = ToastPrimitive.useToastManager

   export {
     Toast,
     ToastAction,
     ToastClose,
     ToastContent,
     ToastDescription,
     ToastIcon,
     ToastPortal,
     ToastProvider,
     ToastTitle,
     ToastViewport,
     Toaster,
     createToastManager,
     toast,
     useToastManager,
   }
   ```

4. Update the import paths to match your project setup.

5. Render the `Toaster` once, before your app. A toast fired while the app
   mounts is dropped if the `Toaster` has not subscribed yet.

   ```tsx title="main.tsx"
   import "./styles/theme.css"
   import "./styles/interop.css"
   import { StrictMode } from "react"
   import { createRoot } from "react-dom/client"
   import { Toaster } from "@/components/ui/toast"
   import App from "./App"

   createRoot(document.getElementById("root")!).render(
     <StrictMode>
       <Toaster />
       <App />
     </StrictMode>,
   )
   ```

## Usage

```tsx
import { toast } from "@/components/ui/toast"
```

```tsx
toast.add({
  title: "Event created",
  description: "Sunday, December 3 at 9:00 AM",
})
```

`toast` is a manager created outside React, so it works from an event handler,
a store or a fetch callback as well as from a component. `toast.add` returns
the toast's `id`. Pass it to `toast.close` or `toast.update`.

## Composition

`Toaster` renders all of this for you. Compose the parts yourself only when the
built-in layout is not the one you need.

```text
ToastProvider
└── ToastPortal
    └── ToastViewport            the fixed landmark the stack lives in
        └── Toast                one per toast in `useToastManager().toasts`
            └── ToastContent
                ├── ToastIcon
                ├── ToastTitle
                ├── ToastDescription
                ├── ToastAction
                └── ToastClose
```

Toasts stack. New toasts appear in front and older ones show below them at a
smaller scale. The stack fans out while the pointer is over it or focus is
inside it. A toast can be swiped down or to the right to dismiss it.

> **Note**
>
> `ToastTitle`, `ToastDescription` and `ToastAction` read from the toast object
> and render nothing when it has no `title`, `description` or `actionProps`, so
> the built-in list needs no conditionals. Past the provider's `limit`, Base UI
> keeps the oldest toast mounted and marks it `data-limited` instead of removing
> it, so it can fade out in place.

## Types

Set the `type` option to render a status icon. The built-in list recognises
`success`, `info`, `warning`, `error` and `loading`.

```tsx
import { Button } from "@/components/ui/button"
import { HStack } from "@/components/ui/stack"
import { toast } from "@/components/ui/toast"

export default function ToastTypes() {
  return (
    <HStack gap={2} wrap="wrap">
      <Button
        variant="outline"
        onClick={() => toast.add({ description: "Event has been created." })}
      >
        Default
      </Button>
      <Button
        variant="outline"
        onClick={() => toast.add({ type: "success", description: "Event has been created." })}
      >
        Success
      </Button>
      <Button
        variant="outline"
        onClick={() =>
          toast.add({ type: "info", description: "Arrive 10 minutes before the event." })
        }
      >
        Info
      </Button>
      <Button
        variant="outline"
        onClick={() =>
          toast.add({ type: "warning", description: "The event cannot start before 8:00 AM." })
        }
      >
        Warning
      </Button>
      <Button
        variant="outline"
        onClick={() =>
          toast.add({
            type: "error",
            description: "The event could not be created.",
            priority: "high",
          })
        }
      >
        Error
      </Button>
    </HStack>
  )
}
```

Pass `priority: "high"` for a message a screen reader should announce at once.
A screen reader announces everything else politely, after it finishes what it
is reading.

## Action

Pass button props with `actionProps` to render an action.

```tsx
const id = toast.add({
  title: "Event created",
  actionProps: {
    children: "Undo",
    onClick() {
      toast.close(id)
    },
  },
})
```

## Promise

Use `toast.promise` to update one toast as an asynchronous task moves through
its loading, success and error states.

```tsx
import { Button } from "@/components/ui/button"
import { toast } from "@/components/ui/toast"

export default function ToastPromise() {
  function showToast() {
    toast.promise(
      new Promise<{ name: string }>((resolve) => {
        window.setTimeout(() => resolve({ name: "Event" }), 2000)
      }),
      {
        loading: "Creating event…",
        success: (data) => `${data.name} created.`,
        error: "Could not create event.",
      },
    )
  }

  return (
    <Button variant="outline" onClick={showToast}>
      Create event
    </Button>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

Every part wraps the Base UI `Toast` part of the same name and accepts its
props. See the [Base UI reference](https://base-ui.com/react/components/toast)
for the manager options, anchored toasts and the primitive API. Each styled part
also takes `xstyle` and `className`. `Toast` and `ToastContent` take `style`
too. It is real inline CSS, merged over whatever the stack's state resolves to.

### Toaster

`Toaster` takes the props of `ToastProvider`.

| Prop           | Type           | Default |
| -------------- | -------------- | ------- |
| `timeout`      | `number`       | `5000`  |
| `limit`        | `number`       | `3`     |
| `toastManager` | `ToastManager` | `toast` |

A `timeout` of `0` keeps a toast up until it is closed. The timer pauses while
the stack is fanned out.

### toast

| Method    | Signature                                           |
| --------- | --------------------------------------------------- |
| `add`     | `(options) => id`                                   |
| `close`   | `(id?) => void`                                     |
| `update`  | `(id, options) => void`                             |
| `promise` | `(promise, { loading, success, error }) => promise` |

`options` takes `title`, `description`, `type`, `timeout`, `priority`,
`actionProps`, `onClose`, `onRemove` and `data`. Adding a toast with an `id`
that is already showing updates it in place and restarts its timer.

### Toast

| Prop             | Type                                                      | Default             |
| ---------------- | --------------------------------------------------------- | ------------------- |
| `toast`          | `ToastObject`                                             | —                   |
| `swipeDirection` | `"up" \| "down" \| "left" \| "right"` or an array of them | `["down", "right"]` |
| `style`          | `CSSProperties`                                           | —                   |

### Data attributes

| Attribute                                  | On                      | Effect                                                  |
| ------------------------------------------ | ----------------------- | ------------------------------------------------------- |
| `data-slot="toast-viewport"`               | the viewport            | The fixed landmark; `F6` moves focus into it            |
| `data-slot="toast"`                        | the root                | The box that stacks, slides and swipes                  |
| `data-slot="toast-content"`                | the content             | The padded row; fades while the toast is behind another |
| `data-slot="toast-icon"`                   | the icon                | Present only for a recognised `type`                    |
| `data-type`                                | the root                | The toast's `type`, as given                            |
| `data-expanded`                            | root, viewport, content | Base UI writes it while the stack is fanned out         |
| `data-limited`                             | the root                | Base UI writes it past the provider's `limit`           |
| `data-swiping`, `data-swipe-direction`     | the root                | Base UI writes them during a swipe                      |
| `data-behind`                              | the content             | Base UI writes it on every toast but the front one      |
| `data-starting-style`, `data-ending-style` | the root                | Base UI writes them while the toast animates            |
