# Toast

> Show brief, noncritical feedback after an action or background event.

Source: https://www.honestui.com/docs/components/toast

```tsx
"use client"

import { Button } from "@/components/honest-ui/ui/button"
import { toastManager } from "@/components/honest-ui/ui/toast"

export function ToastDemo() {
  return (
    <Button
      variant="secondary"
      onClick={() => {
        toastManager.add({
          title: "Event has been created",
          description: "Monday, January 3rd at 6:00pm",
        })
      }}
    >
      Default Toast
    </Button>
  )
}

```

## Overview [#overview]

Use Toast for short feedback that appears after an action completes or a background event fires: saves, copies, uploads, syncs, and lightweight errors. A toast confirms something happened. It must not carry information people need later — anything that changes what they should do next belongs on the page, near the content it affects.

Honest UI includes two toast styles driven by one API:

* **Toast:** a quiet, utilitarian notification built on Base UI. Use it for most product feedback.
* **Gooey Toast:** an animated notification with morphing transitions (`variant: "gooey"`). Use it sparingly, when the notification is part of an expressive product moment such as a celebratory save or upload completion.

Both styles render from the same `toastManager` and share status types, actions, positions, durations, and promise helpers.

## Anatomy [#anatomy]

A toast has a title, optional description, an optional status icon, an optional action button, and a dismiss button. The `ToastProvider` mounts the viewport that renders the stack of active toasts, so it stays installed once near the application root while pages call `toastManager.add` from anywhere.

The stack collapses behind the frontmost toast: older notifications peek out underneath until you hover or move focus to the viewport, which expands the stack so every toast is reachable.

## Behavior [#behavior]

**Statuses.** Pass `type` to match the result: `success`, `error`, `warning`, `info`, or `loading`. The icon and its color follow automatically. Use promise helpers when an async action moves through loading, success, and error — see the [promise example](#promise).

**Timers.** By default a toast auto-dismisses after 5 seconds (gooey after 6). The timer pauses while you point at the stack, while keyboard focus is inside it, and while the browser window is unfocused, then resumes where it left off. Set `timeout: 0` (standard) or `duration: null` (gooey) to keep a toast until dismissed.

**Stack limits.** The provider keeps at most 3 toasts (`limit`). When the limit is reached, the oldest toast is removed to make room — one more reason critical information does not belong here.

**Swipe.** Swiping dismisses a toast. The allowed directions follow the position automatically: edge positions swipe toward their nearest edge, centered positions swipe up (top) or down (bottom).

Choose the standard style when the interface should feel calm. Choose gooey when motion carries meaning for the moment; everything else about the API stays the same.

## Accessibility [#accessibility]

The viewport is a region with an implicit polite live region: new toasts are announced by screen readers without stealing focus from the current task. For messages that must interrupt — a failed background job, for instance — set `priority: "high"`; the toast is then additionally mirrored into a visually hidden `role="alert"` container, which assistive technology announces urgently.

Keyboard users can reach toasts without hunting: pressing <kbd>F6</kbd> anywhere moves focus into the viewport and pauses all timers, <kbd>Tab</kbd> reaches each toast's action and dismiss buttons, and <kbd>Escape</kbd> closes the focused toast.

Keep titles short and put recovery detail in the description. Because timers pause on hover and focus rather than resetting, reading time extends naturally — but never rely on that for long content.

A toast is supplemental feedback. Errors that affect form content must also appear inline near the field or section they belong to, in dark mode and light, since the toast may expire before anyone acts on it. Messages use theme tokens and wrap rather than truncate; localized descriptions longer than two lines are a signal to shorten them.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;toast&#x22;]" />

    
      Add the 

      `ToastProvider`

       to your app.
    

    <span data-lib="base-ui">
      ```tsx title="app/layout.tsx"
      // [!code word:import { ToastProvider } from "@/components/ui/toast"]
      // [!code word:<ToastProvider>]
      // [!code word:</ToastProvider>]
      import { ToastProvider } from "@/components/ui/toast"

      export default function RootLayout({ children }) {
        return (
          <html lang="en">
            <head />
            <body>
              <ToastProvider>
                <main>{children}</main>
              </ToastProvider>
            </body>
          </html>
        )
      }
      ```
    </span>
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/toast.tsx

```tsx
"use client"

import * as React from "react"
import { Toast } from "@base-ui/react/toast"
import {
  CircleAlert as CircleAlertIcon,
  CircleCheck as CircleCheckIcon,
  Info as InfoIcon,
  LoaderCircle as LoaderCircleIcon,
  TriangleAlert as TriangleAlertIcon,
  X as XIcon,
} from "honestui/icons"

import {
  gooey,
  Toaster as GooeyToaster,
  type GooeyOptions,
  type GooeyState,
} from "./toast-gooey"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"

const standardToastManager = Toast.createToastManager()

const TOAST_ICONS = {
  loading: LoaderCircleIcon,
  success: CircleCheckIcon,
  error: CircleAlertIcon,
  info: InfoIcon,
  warning: TriangleAlertIcon,
} as const

type ToastPosition =
  | "top-left"
  | "top-center"
  | "top-right"
  | "bottom-left"
  | "bottom-center"
  | "bottom-right"

type ToastVariant = "default" | "standard" | "gooey"
type StandardToastOptions = Parameters<typeof standardToastManager.add>[0]
type StandardToastUpdateOptions = Parameters<typeof standardToastManager.update>[1]

interface ToastOptions extends StandardToastOptions {
  variant?: ToastVariant
  position?: ToastPosition
  state?: GooeyState
  duration?: GooeyOptions["duration"]
  icon?: GooeyOptions["icon"]
  styles?: GooeyOptions["styles"]
  fill?: GooeyOptions["fill"]
  roundness?: GooeyOptions["roundness"]
  autopilot?: GooeyOptions["autopilot"]
  button?: GooeyOptions["button"]
}

interface ToastUpdateOptions extends StandardToastUpdateOptions {
  variant?: ToastVariant
  position?: ToastPosition
  state?: GooeyState
  duration?: GooeyOptions["duration"]
  icon?: GooeyOptions["icon"]
  styles?: GooeyOptions["styles"]
  fill?: GooeyOptions["fill"]
  roundness?: GooeyOptions["roundness"]
  autopilot?: GooeyOptions["autopilot"]
  button?: GooeyOptions["button"]
}

interface ToastPromiseOptions<Value> {
  variant?: ToastVariant
  position?: ToastPosition
  loading: string | ToastOptions
  success: string | ToastOptions | ((result: Value) => string | ToastOptions)
  error: string | ToastOptions | ((error: unknown) => string | ToastOptions)
}

interface ToastProviderProps extends Toast.Provider.Props {
  position?: ToastPosition
  gooeyPosition?: ToastPosition
  gooeyOptions?: Partial<GooeyOptions>
}

function getGooeyState(type?: string, state?: GooeyState): GooeyState {
  if (state) return state
  if (
    type === "success" ||
    type === "loading" ||
    type === "error" ||
    type === "warning" ||
    type === "info" ||
    type === "action"
  ) {
    return type
  }
  return "success"
}

function getGooeyButton(options: ToastOptions | ToastUpdateOptions) {
  if (options.button) return options.button

  const children = options.actionProps?.children
  const onClick = options.actionProps?.onClick
  if (typeof children !== "string" || !onClick) return undefined

  return {
    title: children,
    onClick: () => onClick({} as React.MouseEvent<HTMLButtonElement>),
  }
}

function toGooeyOptions(options: ToastOptions | ToastUpdateOptions): GooeyOptions {
  return {
    title: typeof options.title === "string" ? options.title : undefined,
    description: options.description,
    position: options.position,
    duration:
      options.duration ??
      (options.timeout === 0 ? null : options.timeout),
    icon: options.icon,
    styles: options.styles,
    fill: options.fill,
    roundness: options.roundness,
    autopilot: options.autopilot,
    button: getGooeyButton(options),
  }
}

function addGooeyToast(options: ToastOptions) {
  const state = getGooeyState(options.type, options.state)
  const config = toGooeyOptions(options)

  if (state === "loading") {
    return (gooey.show as (opts: GooeyOptions & { state: GooeyState }) => string)(
      { ...config, state }
    )
  }
  return gooey[state](config)
}

function toStandardOptions(options: ToastOptions | ToastUpdateOptions) {
  const standardOptions = { ...options }

  delete standardOptions.variant
  delete standardOptions.position
  delete standardOptions.state
  delete standardOptions.duration
  delete standardOptions.icon
  delete standardOptions.styles
  delete standardOptions.fill
  delete standardOptions.roundness
  delete standardOptions.autopilot
  delete standardOptions.button

  return standardOptions
}

function resolvePromiseOption<Value>(
  option: ToastPromiseOptions<Value>["success"],
  value: Value
) {
  return typeof option === "function" ? option(value) : option
}

function normalizePromiseOption(option: string | ToastOptions) {
  return typeof option === "string" ? { title: option } : option
}

const toastManager = {
  add(options: ToastOptions) {
    if (options.variant === "gooey") return addGooeyToast(options)
    return standardToastManager.add(toStandardOptions(options))
  },
  close(id: string) {
    standardToastManager.close(id)
    gooey.dismiss(id)
  },
  update(id: string, options: ToastUpdateOptions) {
    if (options.variant === "gooey") {
      gooey.dismiss(id)
      addGooeyToast(options as ToastOptions)
      return
    }

    standardToastManager.update(id, toStandardOptions(options))
  },
  promise<Value>(
    promiseValue: Promise<Value>,
    options: ToastPromiseOptions<Value>
  ) {
    if (options.variant === "gooey") {
      return gooey.promise(promiseValue, {
        position: options.position,
        loading: toGooeyOptions(normalizePromiseOption(options.loading)),
        success: (result: Value) =>
          toGooeyOptions(
            normalizePromiseOption(resolvePromiseOption(options.success, result))
          ),
        error: (error: unknown) =>
          toGooeyOptions(
            normalizePromiseOption(resolvePromiseOption(options.error, error))
          ),
      })
    }

    return standardToastManager.promise(promiseValue, {
      loading: toStandardOptions(normalizePromiseOption(options.loading)),
      success: (result: Value) =>
        toStandardOptions(
          normalizePromiseOption(resolvePromiseOption(options.success, result))
        ),
      error: (error: unknown) =>
        toStandardOptions(
          normalizePromiseOption(resolvePromiseOption(options.error, error))
        ),
    })
  },
}

function ToastProvider({
  children,
  position = "bottom-right",
  gooeyPosition = "top-right",
  gooeyOptions,
  ...props
}: ToastProviderProps) {
  return (
    <Toast.Provider toastManager={standardToastManager} {...props}>
      {children}
      <ToastList position={position} />
      <GooeyToaster position={gooeyPosition} options={gooeyOptions} />
    </Toast.Provider>
  )
}

function ToastList({ position = "bottom-right" }: { position: ToastPosition }) {
  const { toasts } = Toast.useToastManager()
  const isTop = position.startsWith("top")

  return (
    <Toast.Portal data-slot="toast-portal">
      <Toast.Viewport
        className={cn(
          "fixed z-[var(--hui-z-index-toast)] w-[360px] max-w-[calc(100vw-var(--hui-space-10))] outline-none [--gap:var(--hui-space-4)]",
          "data-[position=top-left]:top-[var(--hui-space-7)] data-[position=top-left]:left-[var(--hui-space-7)]",
          "data-[position=top-center]:top-[var(--hui-space-7)] data-[position=top-center]:left-1/2 data-[position=top-center]:-translate-x-1/2",
          "data-[position=top-right]:top-[var(--hui-space-7)] data-[position=top-right]:right-[var(--hui-space-7)]",
          "data-[position=bottom-left]:bottom-[var(--hui-space-7)] data-[position=bottom-left]:left-[var(--hui-space-7)]",
          "data-[position=bottom-center]:bottom-[var(--hui-space-7)] data-[position=bottom-center]:left-1/2 data-[position=bottom-center]:-translate-x-1/2",
          "data-[position=bottom-right]:right-[var(--hui-space-7)] data-[position=bottom-right]:bottom-[var(--hui-space-7)]"
        )}
        data-slot="toast-viewport"
        data-position={position}
      >
        {toasts.map((toast) => {
          const Icon = toast.type
            ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
            : null

          return (
            <Toast.Root
              key={toast.id}
              toast={toast}
              data-position={position}
              swipeDirection={
                position.includes("center")
                  ? [isTop ? "up" : "down"]
                  : position.includes("left")
                    ? ["left", isTop ? "up" : "down"]
                    : ["right", isTop ? "up" : "down"]
              }
              className={cn(
                "absolute box-border h-[var(--height)] w-full cursor-default overflow-clip rounded-[var(--hui-radius-2)] border-[0.5px] border-[var(--hui-color-border-base-primary)] bg-[var(--hui-color-background-base-primary)] bg-clip-padding p-[var(--hui-space-3)] text-[var(--hui-color-foreground-base-primary)] shadow-[var(--hui-shadow-lifted)] select-none [--height:var(--toast-frontmost-height,var(--toast-height))] [--peek:var(--hui-space-4)] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [transition:opacity_var(--hui-duration-slow)_var(--hui-ease-out)] z-[calc(var(--hui-z-index-toast)-var(--toast-index))] motion-safe:[transition:transform_var(--hui-duration-slow)_var(--hui-ease-out),opacity_var(--hui-duration-slow)_var(--hui-ease-out),height_var(--hui-duration-fast)_var(--hui-ease-out)] data-swiping:[transition:none]!",
                "data-[position*=right]:right-0 data-[position*=right]:left-auto data-[position*=left]:right-auto data-[position*=left]:left-0 data-[position*=center]:right-0 data-[position*=center]:left-0",
                "data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-bottom data-[position*=bottom]:[--offset-y:calc(var(--toast-offset-y)*-1+(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] data-[position*=bottom]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))]",
                "data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-top data-[position*=top]:[--offset-y:calc(var(--toast-offset-y)+(var(--toast-index)*var(--gap))+var(--toast-swipe-movement-y))] data-[position*=top]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--peek))+(var(--shrink)*var(--height))))_scale(var(--scale))]",
                "after:absolute after:left-0 after:w-full after:content-[''] data-[position*=bottom]:after:bottom-full data-[position*=bottom]:after:h-[calc(var(--gap)+1px)] data-[position*=top]:after:top-full data-[position*=top]:after:h-[calc(var(--gap)+1px)]",
                "data-expanded:h-[var(--toast-height)] data-expanded:[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--offset-y))]",
                "data-starting-style:opacity-0 data-limited:opacity-0 data-ending-style:opacity-0",
                "data-[position*=bottom]:data-starting-style:[transform:translateY(calc(100%+var(--hui-space-7)))] data-[position*=top]:data-starting-style:[transform:translateY(calc(-100%-var(--hui-space-7)))]",
                "data-[position*=bottom]:data-ending-style:not-data-swipe-direction:[transform:translateY(calc(100%+var(--hui-space-7)))] data-[position*=top]:data-ending-style:not-data-swipe-direction:[transform:translateY(calc(-100%-var(--hui-space-7)))]",
                "data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))] data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]",
                "data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]"
              )}
            >
              <Toast.Content className="flex items-start gap-[var(--hui-space-3)] overflow-hidden [transition:opacity_var(--hui-duration-moderate)_var(--hui-ease-out)] data-behind:pointer-events-none data-behind:opacity-0 data-expanded:pointer-events-auto data-expanded:opacity-100">
                {Icon && (
                  <div
                    className="inline-flex min-h-[var(--hui-space-7)] w-[var(--hui-space-5)] shrink-0 items-center justify-center text-[var(--hui-color-foreground-base-secondary)] in-data-[type=error]:text-[var(--hui-color-foreground-danger-primary)] in-data-[type=info]:text-[var(--hui-color-foreground-accent-primary)] in-data-[type=success]:text-[var(--hui-color-foreground-success-primary)] in-data-[type=warning]:text-[var(--hui-color-foreground-attention-primary)] [&>svg]:size-[var(--hui-space-5)] [&>svg]:shrink-0 in-data-[type=loading]:[&>svg]:animate-spin"
                    data-slot="toast-icon"
                  >
                    <Icon />
                  </div>
                )}

                <div className="min-w-0 flex-1" data-slot="toast-main">
                  <div className="flex min-h-[var(--hui-space-7)] items-center justify-between gap-[var(--hui-space-3)]">
                    <Toast.Title
                      className="m-0 min-w-0 flex-1 text-[var(--hui-color-foreground-base-primary)] [font-size:var(--hui-font-size-regular)]! [font-weight:var(--hui-font-weight-medium)] [letter-spacing:var(--hui-letter-spacing-regular)] [line-height:var(--hui-line-height-regular)]! [text-wrap:wrap]!"
                      data-slot="toast-title"
                    />
                    <div
                      className="flex shrink-0 items-center gap-[var(--hui-space-1)]"
                      data-slot="toast-actions"
                    >
                      {toast.actionProps && (
                        <Toast.Action
                          className={buttonVariants({
                            variant: "secondary",
                            size: "xs",
                          })}
                          data-slot="toast-action"
                        >
                          {toast.actionProps.children}
                        </Toast.Action>
                      )}
                      <Toast.Close
                        aria-label="Dismiss notification"
                        className={buttonVariants({
                          variant: "link",
                          size: "icon-sm",
                        })}
                        data-slot="toast-close"
                      >
                        <XIcon />
                      </Toast.Close>
                    </div>
                  </div>
                  <Toast.Description
                    className="m-0 text-[var(--hui-color-foreground-base-primary)] [font-size:var(--hui-font-size-small)] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)]"
                    data-slot="toast-description"
                  />
                </div>
              </Toast.Content>
            </Toast.Root>
          )
        })}
      </Toast.Viewport>
    </Toast.Portal>
  )
}

export { ToastProvider, type ToastPosition, type ToastVariant, toastManager }

```

      
        Update the import paths to match your project setup.
      

      
        Add the 

        `ToastProvider`

         to your app.
      

      <span data-lib="base-ui">
        ```tsx title="app/layout.tsx"
        // [!code word:import { ToastProvider } from "@/rcomponents/ui/toast"]
        // [!code word:<ToastProvider>]
        // [!code word:</ToastProvider>]
        import { ToastProvider } from "@/components/ui/toast"

        export default function RootLayout({ children }) {
          return (
            <html lang="en">
              <head />
              <body>
                <ToastProvider>
                  <main>{children}</main>
                </ToastProvider>
              </body>
            </html>
          )
        }
        ```
      </span>
    
  


## Usage [#usage]

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

```tsx
toastManager.add({
  title: "Event has been created",
  description: "Monday, January 3rd at 6:00pm",
});
```

By default, standard toasts appear in the **bottom-right** corner and gooey toasts in the **top-right**. Change either per provider:

```tsx
<ToastProvider position="top-center">{children}</ToastProvider>
```

Allowed values for both: `top-left`, `top-center`, `top-right`, `bottom-left`, `bottom-center`, `bottom-right`. A single call can also override the position with its own `position` option.

Add a status with `type`:

```tsx
toastManager.add({
  type: "success",
  title: "Saved",
  description: "Your changes are live.",
});
```

Use `variant: "gooey"` for the animated style from the same API. It combines with statuses, actions, positions, durations, and promise helpers:

```tsx
toastManager.add({
  variant: "gooey",
  type: "success",
  title: "Saved",
  description: "Your changes are live.",
  position: "top-right",
});
```

## Don't do this [#dont-do-this]

### Critical errors that require action [#critical-errors-that-require-action]

```tsx
// Bad
toastManager.add({
  type: "error",
  title: "Payment failed",
  description: "Your card was declined.",
});
```

```tsx
// Good
> 
  <AlertTitle>Payment failed</AlertTitle>
  <AlertDescription>Your card was declined.</AlertDescription>
  <AlertAction>
    <Button>Update payment method</Button>
  </AlertAction>

```

Toasts vanish — after five seconds the decline no longer exists anywhere on screen, yet checkout cannot continue without a response. When the message demands action or blocks progress, put it inline next to the affected content, or use Alert Dialog when work must stop until the person decides.

### Auto-dismissing before the message is read [#auto-dismissing-before-the-message-is-read]

```tsx
// Bad
toastManager.add({ title: error.message, timeout: 1200 });
toastManager.add({ title: "Syncing…", timeout: 0 });
```

```tsx
// Good
toastManager.add({ title: "Invitation not sent", type: "error", timeout: 8000 });
toastManager.add({ title: "Sync complete", description: "All records backed up." });
```

A 1.2-second toast is gone before anyone finishes reading it, and errors deserve extra time because people reread them. The inverse mistake is just as real: pinning everything with `timeout: 0` floods the three-slot stack and silently evicts older toasts. Give errors roughly eight seconds, let routine confirmations take the default, and reserve sticky toasts for genuinely ongoing states like uploads.

### Confirming destruction without undo [#confirming-destruction-without-undo]

```tsx
// Bad
await deleteFile(id);
toastManager.add({ title: "File deleted" });
```

```tsx
// Good
await deleteFile(id);
toastManager.add({
  title: "File deleted",
  description: "Moved to trash.",
  actionProps: {
    children: "Undo",
    onClick: () => restoreFile(id),
  },
});
```

A fire-and-forget "Deleted!" tells people the loss is permanent the instant it happens. If the operation can be soft-deleted or reversed within a window, surface that escape hatch directly on the toast so recovery takes one click instead of a search through documentation.

## Examples [#examples]

### With Status [#with-status]

Success, error, info, warning, and loading icons driven by `type`.

```tsx
"use client"

import { Button } from "@/components/honest-ui/ui/button"
import { toastManager } from "@/components/honest-ui/ui/toast"

export function ToastWithStatus() {
  return (
    <div className="flex flex-wrap gap-2">
      <Button
        variant="secondary"
        onClick={() => {
          toastManager.add({
            title: "Changes saved",
            description: "Your notification settings are up to date.",
            type: "success",
          })
        }}
      >
        Show success
      </Button>
      <Button
        variant="secondary"
        onClick={() => {
          toastManager.add({
            title: "Invitation not sent",
            description: "Check the email address, then try again.",
            type: "error",
          })
        }}
      >
        Show error
      </Button>
      <Button
        variant="secondary"
        onClick={() => {
          toastManager.add({
            title: "Component copied",
            description: "Review the new source file before committing it.",
            type: "info",
          })
        }}
      >
        Show information
      </Button>
      <Button
        variant="secondary"
        onClick={() => {
          toastManager.add({
            title: "Session expires in 5 minutes",
            description: "Save your work to avoid losing unsaved changes.",
            type: "warning",
          })
        }}
      >
        Show warning
      </Button>
    </div>
  )
}

```

### Loading [#loading]

An in-progress toast with the spinner icon.

```tsx
"use client"

import { Button } from "@/components/honest-ui/ui/button"
import { toastManager } from "@/components/honest-ui/ui/toast"

export function ToastLoading() {
  return (
    <Button
      variant="secondary"
      onClick={() => {
        toastManager.add({
          title: "Loading…",
          description: "Please wait while we process your request.",
          type: "loading",
        })
      }}
    >
      Loading Toast
    </Button>
  )
}

```

### With Action [#with-action]

An Undo button on the toast itself; activating it closes the toast and reports the reversal.

```tsx
"use client"

import { Button } from "@/components/honest-ui/ui/button"
import { toastManager } from "@/components/honest-ui/ui/toast"

export function ToastWithAction() {
  return (
    <Button
      variant="secondary"
      onClick={() => {
        const id = toastManager.add({
          title: "Action performed",
          description: "You can undo this action.",
          type: "success",
          actionProps: {
            children: "Undo",
            onClick: () => {
              toastManager.close(id)
              toastManager.add({
                title: "Action undone",
                description: "The action has been reverted.",
                type: "info",
              })
            },
          },
          timeout: 1000000,
        })
      }}
    >
      Perform Action
    </Button>
  )
}

```

### Promise [#promise]

One call drives loading, success, and error states from a single promise.

```tsx
"use client";

import { Button } from "@/components/honest-ui/ui/button";
import { toastManager } from "@/components/honest-ui/ui/toast";

export function ToastPromise() {
  function showResult(result: "success" | "error") {
    const request = new Promise<string>((resolve, reject) => {
      setTimeout(() => {
        if (result === "success") resolve("Report loaded");
        else reject(new Error("Report request failed"));
      }, 900);
    });

    toastManager.promise(request, {
      loading: {
        title: "Loading report…",
        description: "The report request is in progress.",
      },
      success: (data: string) => ({
        title: data,
        description: "The latest results are ready to review.",
      }),
      error: () => ({
        title: "Report not loaded",
        description: "Check your connection, then try again.",
      }),
    });
  }

  return (
    <div className="flex flex-wrap gap-[var(--hui-space-3)]">
      <Button variant="secondary" onClick={() => showResult("success")}>
        Load successfully
      </Button>
      <Button variant="outline" onClick={() => showResult("error")}>
        Show failed request
      </Button>
    </div>
  );
}

```

### Varying Heights [#varying-heights]

Stacking behavior when descriptions differ in length; the collapsed stack expands on hover or focus.

```tsx
"use client";

import * as React from "react";

import { Button } from "@/components/honest-ui/ui/button";
import { toastManager } from "@/components/honest-ui/ui/toast";

const TEXTS = [
  "Short message.",
  "A bit longer message that spans two lines.",
  "This is a longer description that intentionally takes more vertical space to demonstrate stacking with varying heights.",
  "An even longer description that should span multiple lines so we can verify the clamped collapsed height and smooth expansion animation when hovering or focusing the viewport.",
];

export function ToastHeights() {
  const [count, setCount] = React.useState(0);

  function createToast() {
    const nextCount = count + 1;
    setCount(nextCount);
    const description = TEXTS[(nextCount - 1) % TEXTS.length];
    toastManager.add({
      title: `Notification ${nextCount}`,
      description,
    });
  }

  return (
    <Button variant="secondary" onClick={createToast}>
      With Varying Heights
    </Button>
  );
}

```

### Gooey States [#gooey-states]

The animated style across success, error, warning, info, and action states.

```tsx
"use client"

import { Button } from "@/components/honest-ui/ui/button"
import { toastManager } from "@/components/honest-ui/ui/toast"

export function GooeyToastStates() {
  return (
    <div className="flex flex-wrap gap-2">
      <Button
        variant="secondary"
        onClick={() => {
          toastManager.add({
            variant: "gooey",
            type: "success",
            title: "Profile saved",
            description: "Your updated name is now visible to workspace members.",
          })
        }}
      >
        Success
      </Button>
      <Button
        variant="secondary"
        onClick={() => {
          toastManager.add({
            variant: "gooey",
            type: "error",
            title: "Upload failed",
            description: "The file is larger than 10 MB. Choose a smaller file and try again.",
          })
        }}
      >
        Error
      </Button>
      <Button
        variant="secondary"
        onClick={() => {
          toastManager.add({
            variant: "gooey",
            type: "warning",
            title: "Storage nearly full",
            description: "2 GB remains in this workspace.",
          })
        }}
      >
        Warning
      </Button>
      <Button
        variant="secondary"
        onClick={() => {
          toastManager.add({
            variant: "gooey",
            type: "info",
            title: "Keyboard shortcut available",
            description: "Press Command and K to open search.",
          })
        }}
      >
        Info
      </Button>
      <Button
        variant="secondary"
        onClick={() => {
          toastManager.add({
            variant: "gooey",
            type: "action",
            title: "Review sign-in methods",
            description: "A new sign-in method was added to this account.",
          })
        }}
      >
        Action
      </Button>
    </div>
  )
}

```

### Gooey Promise [#gooey-promise]

Promise-driven state transitions in the animated style.

```tsx
"use client"

import { Button } from "@/components/honest-ui/ui/button"
import { toastManager } from "@/components/honest-ui/ui/toast"

export function GooeyToastPromise() {
  const handlePromise = () => {
    const promise = new Promise<{ name: string }>((resolve) => {
      setTimeout(() => resolve({ name: "User" }), 2000)
    })

    toastManager.promise(promise, {
      variant: "gooey",
      loading: { title: "Loading..." },
      success: (data) => ({
        title: "Success!",
        description: `Welcome back, ${data.name}!`,
      }),
      error: {
        title: "Error",
        description: "Failed to load data.",
      },
    })
  }

  return (
    <Button variant="secondary" onClick={handlePromise}>
      Load Data
    </Button>
  )
}

```

### Gooey With Action Button [#gooey-with-action-button]

A restorable delete with the action button rendered inside the morphing pill.

```tsx
"use client"

import { Button } from "@/components/honest-ui/ui/button"
import { toastManager } from "@/components/honest-ui/ui/toast"

export function GooeyToastWithButton() {
  return (
    <Button
      variant="secondary"
      onClick={() => {
        const id = toastManager.add({
          variant: "gooey",
          type: "action",
          title: "File deleted",
          description: "Your file has been moved to trash.",
          button: {
            title: "Undo",
            onClick: () => {
              toastManager.close(id)
              toastManager.add({
                variant: "gooey",
                type: "success",
                title: "Restored",
                description: "Your file has been restored.",
              })
            },
          },
        })
      }}
    >
      Delete File
    </Button>
  )
}

```

### Gooey Position [#gooey-position]

The animated style placed from any screen edge.

```tsx
"use client"

import { Button } from "@/components/honest-ui/ui/button"
import { toastManager, type ToastPosition } from "@/components/honest-ui/ui/toast"

export function GooeyToastPosition() {
  const positions: ToastPosition[] = [
    "top-left",
    "top-center",
    "top-right",
    "bottom-left",
    "bottom-center",
    "bottom-right",
  ]

  return (
    <div className="flex flex-wrap gap-2">
      {positions.map((position) => (
        <Button
          key={position}
          variant="secondary"
          onClick={() => {
            toastManager.add({
              variant: "gooey",
              title: position,
              description: `Toast shown at ${position}`,
              position,
            })
          }}
        >
          {position}
        </Button>
      ))}
    </div>
  )
}

```

## API reference [#api-reference]

### `ToastProvider` [#toastprovider]

Installs the viewport once near the application root.

| Prop            | Values                        | Default        |
| --------------- | ----------------------------- | -------------- |
| `position`      | `top-left` … `bottom-right`   | `bottom-right` |
| `gooeyPosition` | `top-left` … `bottom-right`   | `top-right`    |
| `gooeyOptions`  | Partial `GooeyOptions`        | —              |
| `timeout`       | ms; `0` disables auto-dismiss | `5000`         |
| `limit`         | number                        | `3`            |

### `toastManager` [#toastmanager]

| Method    | Signature                                                  |
| --------- | ---------------------------------------------------------- |
| `add`     | `(options: ToastOptions) => string` (returns the toast id) |
| `close`   | `(id: string)`                                             |
| `update`  | `(id: string, options: ToastUpdateOptions)`                |
| `promise` | `(promise, { loading, success, error }) => Promise`        |

### Toast options [#toast-options]

| Option        | Values                                           | Notes                                        |
| ------------- | ------------------------------------------------ | -------------------------------------------- |
| `title`       | ReactNode                                        | Required in practice; keep to one line       |
| `description` | ReactNode                                        | Wraps to multiple lines                      |
| `type`        | `success`, `error`, `warning`, `info`, `loading` | Sets icon and color                          |
| `timeout`     | ms                                               | `0` keeps the toast until dismissed          |
| `priority`    | `"low"`, `"high"`                                | `high` announces urgently via `role="alert"` |
| `actionProps` | `{ children, onClick }`                          | Renders the action button                    |
| `variant`     | `"default"`, `"standard"`, `"gooey"`             | `"gooey"` uses the animated toaster          |
| `position`    | six corner/edge values                           | Overrides the provider position              |

Gooey-only options: `duration` (number or `null`; `null` never auto-dismisses, default `6000`), `icon`, `styles`, `fill`, `roundness`, `autopilot` (`boolean` or `{ expand?, collapse? }` timing in ms), `button` (`{ title, onClick }`), and `state` to force a gooey state directly.

Keep the provider mounted once near the application root. Toasts are supplemental feedback: persist errors or results people may need after the timeout elsewhere, and never use a toast as the only confirmation of a destructive action.
