Primitives
Toast
Transient notifications with a callable toast API and single Toaster mount.
Composition
Application toast host
- <Toaster />
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
The Control UI source installs this primitive from src/registry/sources/control-ui/ui/toast.tsx.
npx shadcn@latest add https://control-ui.dev/r/toast.jsonDependencies
Raw code
Primary installed source
"use client";import type { ToastManagerAddOptions, ToastManagerPromiseOptions, ToastManagerUpdateOptions, ToastObject } from "@base-ui/react/toast";import { Toast as ToastPrimitive } from "@base-ui/react/toast";import { CircleCheckIcon, CircleXIcon, InfoIcon, TriangleAlertIcon } from "lucide-react";import type { CSSProperties, ReactNode } from "react";import type { PopupKnobStyle } from "@/components/control-ui/knob-contracts/popup-knobs";import { cn } from "@/components/control-ui/lib/cn";import { PopupCloseButton } from "@/components/control-ui/popup-parts";import { controlEffectsAttribute } from "@/components/control-ui/skin";import { useSkin } from "@/components/control-ui/skin-provider";import { Button, type ButtonProps } from "@/components/control-ui/ui/button";export type ToasterProps = { className?: string; timeout?: number; limit?: number; rootStyle?: CSSProperties & PopupKnobStyle; indicatorStyle?: CSSProperties & PopupKnobStyle; actionStyle?: ButtonProps["style"]; closeStyle?: ButtonProps["style"]; closeLabel?: string; successLabel?: string; errorLabel?: string; warningLabel?: string; infoLabel?: string;};export const toastManager = ToastPrimitive.createToastManager();type ToastOptions = Omit<ToastManagerAddOptions<object>, "title">;type ToastUpdate = ToastManagerUpdateOptions<object>;const errorDefaults: ToastUpdate = { timeout: 0, priority: "high" };function persistenceDefaults(options: ToastOptions): ToastUpdate | undefined { if (options.type === "error") return errorDefaults; return options.actionProps ? { timeout: 0 } : undefined;}function show(title: ReactNode, options: ToastOptions = {}) { return toastManager.add({ title, ...persistenceDefaults(options), ...options });}function withType(type: string) { return (title: ReactNode, options?: ToastOptions) => show(title, { type, ...options });}function persistentError(update: string | ToastUpdate): ToastUpdate { return typeof update === "string" ? { ...errorDefaults, description: update } : { ...errorDefaults, ...update };}export const toast = Object.assign(show, { success: withType("success"), error: withType("error"), warning: withType("warning"), info: withType("info"), message: (title: ReactNode, options?: ToastOptions) => show(title, options), promise: <Value,>(promise: Promise<Value>, options: ToastManagerPromiseOptions<Value, object>) => toastManager.promise(promise, { ...options, error: (reason: unknown) => persistentError(typeof options.error === "function" ? options.error(reason) : options.error), }), dismiss: (id?: string) => toastManager.close(id),});const statusIcons = { success: CircleCheckIcon, error: CircleXIcon, warning: TriangleAlertIcon, info: InfoIcon,};type ToastStatus = keyof typeof statusIcons;function isToastStatus(type: string | undefined): type is ToastStatus { return type !== undefined && Object.hasOwn(statusIcons, type);}export const useToast = ToastPrimitive.useToastManager;type ToastListProps = Pick<ToasterProps, "rootStyle" | "indicatorStyle" | "actionStyle" | "closeStyle"> & { closeLabel: string; statusLabels: Record<ToastStatus, string>;};function ToastIndicator({ status, style }: { status: ToastStatus | undefined; style: ToasterProps["indicatorStyle"] }) { if (!status) return null; const StatusIcon = statusIcons[status]; return ( <span aria-hidden="true" data-control-ui="toast" data-control-family="popup" data-popup-kind="toast" data-slot="indicator" style={style} className="shrink-0" > <StatusIcon className="size-4" /> </span> );}function ToastText({ entry, statusPrefix }: { entry: ToastObject<object>; statusPrefix: ReactNode }) { return ( <div className="flex min-w-0 flex-1 flex-col"> {entry.title ? ( <ToastPrimitive.Title data-control-ui="toast" data-control-family="popup" data-popup-kind="toast" data-slot="title"> {statusPrefix} {entry.title} </ToastPrimitive.Title> ) : null} {entry.description ? ( <ToastPrimitive.Description data-control-ui="toast" data-control-family="popup" data-popup-kind="toast" data-slot="description"> {entry.title ? null : statusPrefix} {entry.description} </ToastPrimitive.Description> ) : null} </div> );}function ToastList({ rootStyle, indicatorStyle, actionStyle, closeStyle, closeLabel, statusLabels }: ToastListProps) { const { toasts } = ToastPrimitive.useToastManager(); return toasts.map((entry) => { const status = isToastStatus(entry.type) ? entry.type : undefined; const statusPrefix = status ? <span className="sr-only">{`${statusLabels[status]}: `}</span> : null; return ( <ToastPrimitive.Root key={entry.id} toast={entry} data-control-ui="toast" data-control-family="popup" data-popup-kind="toast" data-popup-part="surface" data-type={entry.type ?? "message"} style={rootStyle} data-slot="root" data-surface="floating" className={cn( "[--gap:0.75rem] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))]", "absolute end-0 bottom-0 z-[calc(var(--z-toast)-var(--toast-index))] h-[var(--height)] w-full select-none data-[expanded]:h-[var(--toast-height)]", "after:absolute after:top-full after:start-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']", )} > <ToastPrimitive.Content data-control-ui="toast" data-control-family="popup" data-popup-kind="toast" data-slot="content" className="flex items-start" > <ToastIndicator status={status} style={indicatorStyle} /> <ToastText entry={entry} statusPrefix={statusPrefix} /> {entry.actionProps ? ( <ToastPrimitive.Action style={actionStyle} render={(renderProps) => ( <Button {...renderProps} data-popup-kind="toast" data-popup-part="action" variant="surface" size="xs" /> )} /> ) : null} <ToastPrimitive.Close style={closeStyle} render={(renderProps) => <PopupCloseButton {...renderProps} data-popup-kind="toast" label={closeLabel} variant="quiet" />} /> </ToastPrimitive.Content> </ToastPrimitive.Root> ); });}export function Toaster({ className, timeout, limit, rootStyle, indicatorStyle, actionStyle, closeStyle, closeLabel = "Dismiss notification", successLabel = "Success", errorLabel = "Error", warningLabel = "Warning", infoLabel = "Info",}: ToasterProps) { const skin = useSkin(); return ( <ToastPrimitive.Provider toastManager={toastManager} timeout={timeout} limit={limit}> <ToastPrimitive.Portal> <ToastPrimitive.Viewport data-control-ui="toast" data-control-family="popup" data-popup-kind="toast" data-slot="viewport" data-skin={skin.id} data-effects={controlEffectsAttribute(skin.effects)} className={cn("fixed end-4 bottom-4 z-(--z-toast) mx-auto w-[calc(100vw-2rem)] sm:end-6 sm:bottom-6 sm:w-[22.5rem]", className)} > <ToastList rootStyle={rootStyle} indicatorStyle={indicatorStyle} actionStyle={actionStyle} closeStyle={closeStyle} closeLabel={closeLabel} statusLabels={{ success: successLabel, error: errorLabel, warning: warningLabel, info: infoLabel }} /> </ToastPrimitive.Viewport> </ToastPrimitive.Portal> </ToastPrimitive.Provider> );}Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-popup-* · 44 knobsHow the cascade resolves--cui-popup-title-foreground<color>var(--cui-popup-foreground)--cui-popup-title-font-size<length-percentage>var(--text-heading-3)--cui-popup-title-font-weight<number>var(--font-weight-semibold)--cui-popup-title-line-height<number>var(--text-heading-3--line-height)--cui-popup-title-letter-spacing<length>-0.025em--cui-popup-description-foreground<color>var(--muted-foreground)--cui-popup-close-inset<length-percentage>calc(var(--spacing) * 3)--cui-popup-item-inset<length-percentage>calc(var(--spacing) * 8)--cui-popup-label-padding-block<length-percentage>var(--cui-popup-item-padding-block)--cui-popup-label-padding-inline<length-percentage>var(--cui-popup-item-padding-inline)--cui-popup-label-font-size<length-percentage>var(--text-micro)--cui-popup-label-font-weight<number>var(--font-weight-medium)--cui-popup-label-letter-spacing<length>0.08em--cui-popup-label-text-transformnone | uppercase | lowercase | capitalizeuppercase--cui-popup-separator-inset<length-percentage>calc(var(--popover-padding) * -1)--cui-popup-shortcut-font-size<length-percentage>var(--text-body)--cui-popup-shortcut-letter-spacing<length>0.02em--cui-popup-status-success-foreground<color>var(--success-text)--cui-popup-status-warning-foreground<color>var(--warning-text)--cui-popup-status-info-foreground<color>var(--info-text)--cui-popup-status-error-foreground<color>var(--destructive)--cui-popup-radius<length-percentage>var(--radius-popover)--cui-popup-background<color>var(--popover)--cui-popup-foreground<color>var(--popover-foreground)--cui-popup-border-color<color>var(--control-rim)--cui-popup-border-width<length>var(--control-rim-width)--cui-popup-shadow*var(--shadow-pop)--cui-popup-backdrop-filter*blur(var(--backdrop-blur-popover))--cui-popup-backdrop-background<color>oklch(from var(--foreground) l c h / var(--overlay-opacity))--cui-popup-backdrop-blur<length>var(--backdrop-blur-overlay)--cui-popup-item-radius<length-percentage>var(--radius-popup-item-fit)--cui-popup-item-font-size<length-percentage>var(--text-body)--cui-popup-item-icon-size<length-percentage>1rem--cui-popup-item-padding-block<length-percentage>calc(var(--spacing) * 1)--cui-popup-item-padding-inline<length-percentage>calc(var(--padding-x) * 0.5)--cui-popup-item-min-height<length-percentage>max(var(--target-min), var(--control-h-xs))--cui-popup-item-highlight-shadow*inset 0 0 0 1px oklch(from var(--foreground) l c h / 0.35)--cui-popup-item-foreground<color>var(--foreground)--cui-popup-item-highlight-background<color>var(--hover-fill)--cui-popup-item-highlight-foreground<color>var(--cui-popup-item-foreground)--cui-popup-item-highlight-muted-foreground<color>var(--muted-foreground)--cui-popup-item-disabled-opacity<number>var(--disabled-opacity)--cui-popup-separator-color<color>var(--border)--cui-popup-shortcut-foreground<color>var(--muted-foreground)