Toast

PreviousNext

A succinct message notification.

Docs
roiuiitem

Preview

Loading preview…
registry/brook/ui/toast/toast.tsx
"use client";

import { Toast } from "@base-ui/react/toast";
import { X } from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/utils";
import styles from "./toast.module.css";

const toastManager = Toast.createToastManager();
const anchoredToastManager = Toast.createToastManager();

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

function ToastPortal({ ...props }: Toast.Portal.Props) {
  return <Toast.Portal {...props} />;
}

function ToastViewport({ className, ...props }: Toast.Viewport.Props) {
  return <Toast.Viewport className={cn(styles.viewport, className)} data-slot="toast-viewport" {...props} />;
}

function ToastRoot({ className, ...props }: Toast.Root.Props) {
  return <Toast.Root className={cn(styles.root, className)} {...props} />;
}

function ToastContent({ className, ...props }: Toast.Content.Props) {
  return <Toast.Content className={cn(styles.content, className)} {...props} />;
}

function ToastTitle({ className, ...props }: Toast.Title.Props) {
  return <Toast.Title className={cn(styles.title, className)} {...props} />;
}

function ToastDescription({ className, ...props }: Toast.Description.Props) {
  return <Toast.Description className={cn(styles.description, className)} data-slot="toast-description" {...props} />;
}

function ToastAction({ className, ...props }: Toast.Action.Props) {
  return <Toast.Action className={cn(styles.action, className)} data-slot="toast-action" {...props} />;
}

function ToastClose({
  className,
  render,
  ...props
}: Toast.Close.Props & {
  render?: (props: React.ButtonHTMLAttributes<HTMLButtonElement>) => React.ReactNode;
}) {
  return <Toast.Close className={cn(styles.close, className)} data-slot="toast-close" render={render} {...props} />;
}

function ToastList() {
  const { toasts } = Toast.useToastManager();
  return toasts.map((toast) => (
    <ToastRoot key={toast.id} toast={toast}>
      <ToastContent>
        <div
          style={{
            display: "flex",
            alignItems: "start",
            justifyContent: "space-between",
            gap: "0.5rem",
          }}
        >
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
              <ToastTitle />
              {toast.type && <span className={styles.statusPill} data-type={toast.type} />}
            </div>
            <ToastDescription />
            {toast.actionProps && (
              <ToastAction className={styles.actionButton}>{toast.actionProps.children}</ToastAction>
            )}
          </div>
        </div>
        {toast.data?.showCloseButton === true && (
          <ToastClose
            render={(props: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
              <button {...props} aria-label="Close">
                <X className="h-4 w-4" />
              </button>
            )}
          />
        )}
      </ToastContent>
    </ToastRoot>
  ));
}

function AnchoredToasts() {
  const { toasts } = Toast.useToastManager();

  return (
    <Toast.Portal>
      <Toast.Viewport className={styles.anchoredViewport}>
        {toasts.map((toast) => {
          const positionerProps = toast.positionerProps;

          if (!positionerProps?.anchor) {
            return null;
          }

          return (
            <Toast.Positioner
              className={styles.anchoredPositioner}
              key={toast.id}
              sideOffset={positionerProps.sideOffset ?? 4}
              toast={toast}
            >
              <Toast.Root className={styles.anchoredRoot} toast={toast}>
                <Toast.Content className={styles.anchoredContent}>
                  <Toast.Title className={styles.anchoredTitle} />
                </Toast.Content>
              </Toast.Root>
            </Toast.Positioner>
          );
        })}
      </Toast.Viewport>
    </Toast.Portal>
  );
}

function AnchoredToastProvider({ children }: { children: React.ReactNode }) {
  return (
    <Toast.Provider toastManager={anchoredToastManager}>
      {children}
      <AnchoredToasts />
    </Toast.Provider>
  );
}

export { ToastProvider, toastManager, AnchoredToastProvider, anchoredToastManager };

Installation

npx shadcn@latest add @roiui/toast

Usage

import { Toast } from "@/components/toast"
<Toast />