Propel

Toast

A portaled, auto-dismissing notification queued through a manager hook.

Show code
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ToastProvider, useToast } from "@makeplane/propel/components/toast";
import { X } from "lucide-react";

const closeButton = (
  <IconButton variant="ghost" size="xs" aria-label="Dismiss" icon={<Icon icon={X} />} />
);

function ToastTrigger() {
  const { add } = useToast();
  return (
    <Button
      stretch="auto"
      variant="secondary"
      size="sm"
      label="Show notification"
      onClick={() =>
        add({
          title: "Project created",
          description: "Marketing site is ready for your team.",
          data: { variant: "success" },
        })
      }
    />
  );
}

export default function BasicDemo() {
  return (
    <ToastProvider close={closeButton}>
      <ToastTrigger />
    </ToastProvider>
  );
}

Installation

import { ToastProvider, useToast } from "@makeplane/propel/components/toast";

Usage

import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ToastProvider, useToast } from "@makeplane/propel/components/toast";
import { X } from "lucide-react";

const closeButton = (
  <IconButton variant="ghost" size="xs" aria-label="Dismiss" icon={<Icon icon={X} />} />
);

function ToastTrigger() {
  const { add } = useToast();
  return (
    <Button
      stretch="auto"
      variant="secondary"
      size="sm"
      label="Show notification"
      onClick={() =>
        add({
          title: "Project created",
          description: "Marketing site is ready for your team.",
          data: { variant: "success" },
        })
      }
    />
  );
}

export default function BasicDemo() {
  return (
    <ToastProvider close={closeButton}>
      <ToastTrigger />
    </ToastProvider>
  );
}

Examples

Variants

The required variant in a toast’s data selects its status icon and color: success, danger, info, warning, or neutral.

danger and warning toasts are treated as urgent: they’re announced assertively as role="alertdialog" (screen readers interrupt to read them immediately, instead of waiting politely) and stay open for 8 seconds instead of the usual 5 — enough time to read a failure reason before it auto-dismisses. This is automatic; no extra prop is needed. Tests that queried getByRole("dialog") for those variants should use alertdialog (often with { hidden: true }, since Base UI keeps the card aria-hidden until the viewport is focused).

priority and timeout are derived from variant on add and when an update changes urgency (e.g. danger → success). An explicit value in the same call always wins; restating the same variant (progress updates) leaves an existing custom timeout or priority alone — including timeout: 0 for persistent progress toasts.

Show code
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { type ToastVariant, ToastProvider, useToast } from "@makeplane/propel/components/toast";
import { X } from "lucide-react";

const closeButton = (
  <IconButton variant="ghost" size="xs" aria-label="Dismiss" icon={<Icon icon={X} />} />
);

const VARIANTS: { variant: ToastVariant; label: string; title: string; description: string }[] = [
  {
    variant: "success",
    label: "Success",
    title: "Project created",
    description: "Marketing site is ready for your team.",
  },
  {
    variant: "danger",
    label: "Danger",
    title: "Remove failed",
    description: "Could not remove the workspace member.",
  },
  {
    variant: "info",
    label: "Info",
    title: "Import in progress",
    description: "We're syncing your issues from GitHub.",
  },
  {
    variant: "warning",
    label: "Warning",
    title: "Storage almost full",
    description: "You've used 90% of your workspace storage.",
  },
  {
    variant: "neutral",
    label: "Neutral",
    title: "Draft saved",
    description: "Your changes are stored locally.",
  },
];

function VariantTriggers() {
  const { add } = useToast();
  return (
    <div className="flex flex-wrap items-center gap-3">
      {VARIANTS.map(({ variant, label, title, description }) => (
        <Button
          key={variant}
          stretch="auto"
          variant="secondary"
          size="sm"
          label={label}
          onClick={() => add({ title, description, data: { variant } })}
        />
      ))}
    </div>
  );
}

export default function VariantsDemo() {
  return (
    <ToastProvider close={closeButton}>
      <VariantTriggers />
    </ToastProvider>
  );
}

With actions

Pass actions for a left-aligned cluster of up to two buttons, and primaryAction for a right-aligned button.

Show code
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ToastProvider, useToast } from "@makeplane/propel/components/toast";
import { X } from "lucide-react";

const closeButton = (
  <IconButton variant="ghost" size="xs" aria-label="Dismiss" icon={<Icon icon={X} />} />
);

function ActionTrigger() {
  const { add } = useToast();
  return (
    <Button
      stretch="auto"
      variant="secondary"
      size="sm"
      label="Mark as done"
      onClick={() =>
        add({
          title: "Issue moved to Done",
          description: "PROJ-142 was marked complete.",
          data: {
            variant: "success",
            actions: [{ label: "Undo" }],
            primaryAction: { label: "View" },
          },
        })
      }
    />
  );
}

export default function WithActionsDemo() {
  return (
    <ToastProvider close={closeButton}>
      <ActionTrigger />
    </ToastProvider>
  );
}

With progress

Set progress (0–100) to report a long-running task; a thin bar renders between the description and the action row.

Show code
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ToastProvider, useToast } from "@makeplane/propel/components/toast";
import { X } from "lucide-react";

const closeButton = (
  <IconButton variant="ghost" size="xs" aria-label="Dismiss" icon={<Icon icon={X} />} />
);

function ProgressTrigger() {
  const { add } = useToast();
  return (
    <Button
      stretch="auto"
      variant="secondary"
      size="sm"
      label="Export workspace"
      onClick={() =>
        add({
          title: "Exporting workspace",
          description: "Preparing your data for download.",
          data: {
            variant: "info",
            progress: 32,
            actions: [{ label: "Cancel" }],
          },
        })
      }
    />
  );
}

export default function WithProgressDemo() {
  return (
    <ToastProvider close={closeButton}>
      <ProgressTrigger />
    </ToastProvider>
  );
}

Promise

useToast().promise(promise, { loading, success, error }) queues a loading toast, then updates it in place when the promise settles.

Show code
import { Button } from "@makeplane/propel/components/button";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { type ToastData, ToastProvider, useToast } from "@makeplane/propel/components/toast";
import { X } from "lucide-react";

const closeButton = (
  <IconButton variant="ghost" size="xs" aria-label="Dismiss" icon={<Icon icon={X} />} />
);

function PromiseTrigger() {
  const { promise } = useToast();
  return (
    <Button
      stretch="auto"
      variant="secondary"
      size="sm"
      label="Upload files"
      onClick={() =>
        void promise<string, ToastData>(
          new Promise<string>((resolve) => {
            setTimeout(() => resolve("3 attachments"), 1500);
          }),
          {
            loading: {
              title: "Uploading files",
              description: "Your attachments are on their way.",
              data: { variant: "neutral" },
            },
            success: (uploaded) => ({
              title: "Upload complete",
              description: `${uploaded} uploaded.`,
              data: { variant: "success" },
            }),
            error: {
              title: "Upload failed",
              description: "Something went wrong — try again.",
              data: { variant: "danger" },
            },
          },
        )
      }
    />
  );
}

export default function PromiseDemo() {
  return (
    <ToastProvider close={closeButton}>
      <PromiseTrigger />
    </ToastProvider>
  );
}

API Reference

ToastProvider

Wraps the app and renders the toast viewport. Mount it once near the root, then queue toasts with `useToast().add({ title, description, data: { variant } })`. Composes the atomic `elements/toast` parts (Provider + Portal + Viewport) and the manager-driven {@link ToastList}.

PropTypeDefaultDescription
close(required)ReactElement<unknown, string | JSXElementConstructor<any>>The close control (e.g. an `IconButton`) rendered as each toast's close button. It carries its own — localizable — `aria-label`; the toast bakes no label or glyph.
toastManagerToastManager<ToastData>Optional external manager. Must queue Propel `ToastData` so every toast has a `variant`.

Toast

A single styled toast: status icon (auto-selected from `toast.data.variant`), title, description, optional action buttons (from `toast.data.actions` / `primaryAction`), and a close button. Rendered automatically by `ToastProvider` for each queued toast — you normally don't render this directly.

PropTypeDefaultDescription
close(required)ReactElement<unknown, string | JSXElementConstructor<any>>The close control (e.g. an `IconButton`), rendered as the toast's close button. It carries its own — localizable — `aria-label`; the toast bakes no label or glyph.
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ToastRootState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render.