Toast
shadcn's toast is Toast plus a useToast() store and a Toaster that renders the store.
shadcn's toast is Toast plus a useToast() store and a Toaster that renders the store. createToaster is that
store (ids, de-duplication by key, a limit with a waiting list, promise toasts, exit animations), so the port keeps
shadcn's names and replaces the hand-written reducer.
src/ui/toast.tsxtsx
import { createToaster, Toast as ToastPrimitive, type ToastOptions } from "@shadow-js/primitives";
import { For, Show } from "@shadow-js/web";
import { variants } from "../lib/utils";
import { XIcon } from "./icons";
export const toastVariants = variants(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[state=closed]:animate-out data-[state=open]:animate-in",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: { variant: "default" },
},
);
export interface ToastData {
readonly variant?: "default" | "destructive" | undefined;
}
/** One queue for the application, as with shadcn's `useToast` store. */
export const toaster = createToaster<ToastData>({ limit: 3, duration: 5000 });
export function toast(options: ToastOptions<ToastData>): string {
return toaster.add(options);
}
export function Toaster() {
return (
<ToastPrimitive.Provider label="Notifications">
<For each={toaster.toasts()} key={(item) => item.id}>
{(item) => (
<ToastPrimitive
class={toastVariants({ variant: item().data?.variant })}
open={item().open}
duration={item().duration}
onOpenChange={(open: boolean) => {
if (!open) toaster.dismiss(item().id);
}}
onExitComplete={() => toaster.remove(item().id)}
>
<div class="grid gap-1">
<ToastPrimitive.Title class="text-sm font-semibold">{item().title}</ToastPrimitive.Title>
<Show when={item().description !== undefined}>
<ToastPrimitive.Description class="text-sm opacity-90">{item().description}</ToastPrimitive.Description>
</Show>
</div>
<ToastPrimitive.Close
aria-label="Close"
class="absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100"
>
<XIcon class="h-4 w-4" />
</ToastPrimitive.Close>
</ToastPrimitive>
)}
</For>
<ToastPrimitive.Viewport class="fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]" />
</ToastPrimitive.Provider>
);
}