Merino crew sweater
Oat heather
$118.00
Size
Free, copy-and-go Toast components built on the SevenUI Toast primitive.Read the primitive docs.
"use client";
import { CheckIcon, CopyIcon, LinkIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Toast,
ToastContent,
ToastPortal,
ToastProvider,
ToastTitle,
ToastViewport,
createToastManager,
useToastManager,
} from "@/components/ui/toast";
const shareUrl = "https://acme.app/share/q3-roadmap";
const toastManager = createToastManager();
function CompactToasts() {
const { toasts } = useToastManager();
return toasts.map((toastItem) => (
<Toast
key={toastItem.id}
toast={toastItem}
className="rounded-full border-transparent bg-foreground text-background shadow-md focus-visible:ring-ring"
>
<ToastContent className="gap-2 px-4 py-2.5">
<CheckIcon aria-hidden="true" className="size-4 shrink-0" />
<ToastTitle className="truncate" />
</ToastContent>
</Toast>
));
}
export default function Toast01() {
const copyLink = () => {
navigator.clipboard?.writeText(shareUrl).catch(() => {});
toastManager.add({
id: "link-copied",
title: "Link copied to clipboard",
timeout: 2000,
});
};
return (
<ToastProvider toastManager={toastManager}>
<div className="flex w-full max-w-sm items-center gap-2 rounded-lg border border-border bg-background p-1.5 pl-3 shadow-xs">
<LinkIcon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground"
/>
<span className="min-w-0 flex-1 truncate text-sm text-muted-foreground">
{shareUrl}
</span>
<Button size="sm" variant="secondary" onClick={copyLink}>
<CopyIcon aria-hidden="true" />
Copy link
</Button>
</div>
<ToastPortal>
<ToastViewport>
<CompactToasts />
</ToastViewport>
</ToastPortal>
</ToastProvider>
);
}
npx shadcn@latest add @sevenui/component/toast-01pnpm dlx shadcn@latest add @sevenui/component/toast-01yarn dlx shadcn@latest add @sevenui/component/toast-01bunx --bun shadcn@latest add @sevenui/component/toast-01"use client";
import {
CircleAlertIcon,
CircleCheckIcon,
InfoIcon,
TriangleAlertIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Toast,
ToastClose,
ToastContent,
ToastDescription,
ToastPortal,
ToastProvider,
ToastTitle,
ToastViewport,
createToastManager,
useToastManager,
} from "@/components/ui/toast";
type Status = "success" | "info" | "warning" | "error";
const statuses: Record<
Status,
{
label: string;
icon: typeof InfoIcon;
tile: string;
title: string;
description: string;
}
> = {
success: {
label: "Success",
icon: CircleCheckIcon,
tile: "bg-success/12 text-success",
title: "Invoice INV-2048 sent",
description: "Delivered to billing@northwind.io.",
},
info: {
label: "Info",
icon: InfoIcon,
tile: "bg-primary/10 text-primary",
title: "Maintenance on Saturday",
description: "The API is read-only from 02:00 to 03:00 UTC.",
},
warning: {
label: "Warning",
icon: TriangleAlertIcon,
tile: "bg-warning/20 text-warning-foreground dark:text-warning",
title: "Storage almost full",
description: "4.5 of 5 GB used on the Team plan.",
},
error: {
label: "Error",
icon: CircleAlertIcon,
tile: "bg-destructive/10 text-destructive",
title: "Payment failed",
description: "Card ending in 4242 was declined. Update your billing details.",
},
};
const order: Status[] = ["success", "info", "warning", "error"];
const toastManager = createToastManager();
function StatusToasts() {
const { toasts } = useToastManager();
return toasts.map((toastItem) => {
const status = statuses[(toastItem.type as Status) ?? "info"];
const Icon = status.icon;
return (
<Toast key={toastItem.id} toast={toastItem}>
<ToastContent className="items-start">
<span
className={`flex size-9 shrink-0 items-center justify-center rounded-lg ${status.tile}`}
>
<Icon aria-hidden="true" className="size-4.5" />
</span>
<div className="flex min-w-0 flex-1 flex-col gap-0.5 pt-0.5">
<ToastTitle />
<ToastDescription className="text-pretty" />
</div>
<ToastClose className="-mt-1 -mr-1" />
</ToastContent>
</Toast>
);
});
}
export default function Toast02() {
return (
<ToastProvider toastManager={toastManager}>
<div className="grid w-full max-w-md grid-cols-2 gap-2 sm:grid-cols-4">
{order.map((type) => {
const status = statuses[type];
const Icon = status.icon;
return (
<Button
key={type}
variant="outline"
onClick={() =>
toastManager.add({
type,
title: status.title,
description: status.description,
priority: type === "error" ? "high" : "low",
})
}
>
<Icon aria-hidden="true" />
{status.label}
</Button>
);
})}
</div>
<ToastPortal>
<ToastViewport>
<StatusToasts />
</ToastViewport>
</ToastPortal>
</ToastProvider>
);
}
npx shadcn@latest add @sevenui/component/toast-02pnpm dlx shadcn@latest add @sevenui/component/toast-02yarn dlx shadcn@latest add @sevenui/component/toast-02bunx --bun shadcn@latest add @sevenui/component/toast-02Acme Desktop 2.3.2 is installed.
"use client";
import * as React from "react";
import { DownloadIcon, RefreshCwIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Toast,
ToastAction,
ToastClose,
ToastContent,
ToastDescription,
ToastPortal,
ToastProvider,
ToastTitle,
ToastViewport,
createToastManager,
useToastManager,
} from "@/components/ui/toast";
const nextVersion = "2.4.0";
const toastManager = createToastManager();
function UpdateToasts() {
const { toasts } = useToastManager();
return toasts.map((toastItem) => (
<Toast key={toastItem.id} toast={toastItem}>
<ToastContent className="flex-col items-stretch gap-3">
<div className="flex items-start gap-3">
<span className="flex size-9 shrink-0 items-center justify-center rounded-full bg-muted">
<DownloadIcon aria-hidden="true" className="size-4" />
</span>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<ToastTitle />
<ToastDescription className="text-pretty" />
</div>
</div>
<div className="flex justify-end gap-2 border-t border-border pt-3">
<ToastClose
aria-label={undefined}
render={<Button variant="ghost" size="sm" />}
className="text-foreground after:hidden"
>
Remind me later
</ToastClose>
<ToastAction render={<Button size="sm" />} />
</div>
</ToastContent>
</Toast>
));
}
export default function Toast03() {
const [status, setStatus] = React.useState<"current" | "restarting">(
"current",
);
const showUpdate = () => {
const id = toastManager.add({
id: "desktop-update",
title: `Version ${nextVersion} is ready`,
description:
"Includes offline drafts and faster search. Restart to finish installing.",
timeout: 0,
actionProps: {
children: "Restart now",
onClick: () => {
setStatus("restarting");
toastManager.close(id);
},
},
});
};
return (
<ToastProvider toastManager={toastManager}>
<div className="flex w-full max-w-xs flex-col items-center gap-3 text-center">
<p className="text-sm text-muted-foreground" aria-live="polite">
{status === "current"
? "Acme Desktop 2.3.2 is installed."
: `Restarting to apply ${nextVersion}…`}
</p>
<Button
variant="outline"
onClick={() => {
setStatus("current");
showUpdate();
}}
>
<RefreshCwIcon aria-hidden="true" />
Check for updates
</Button>
</div>
<ToastPortal>
<ToastViewport>
<UpdateToasts />
</ToastViewport>
</ToastPortal>
</ToastProvider>
);
}
npx shadcn@latest add @sevenui/component/toast-03pnpm dlx shadcn@latest add @sevenui/component/toast-03yarn dlx shadcn@latest add @sevenui/component/toast-03bunx --bun shadcn@latest add @sevenui/component/toast-03“I’ve refunded the second charge of $49.00. It should reach your card in 3–5 business days.”
"use client";
import * as React from "react";
import {
CircleCheckIcon,
LifeBuoyIcon,
ThumbsDownIcon,
ThumbsUpIcon,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Toast,
ToastClose,
ToastContent,
ToastDescription,
ToastPortal,
ToastProvider,
ToastTitle,
ToastViewport,
createToastManager,
useToastManager,
} from "@/components/ui/toast";
type Answer = "yes" | "no";
const ticket = {
id: "4821",
subject: "Refund for a duplicate charge",
agent: "Nora Ellis",
};
const FEEDBACK_ID = "ticket-feedback";
const toastManager = createToastManager();
function FeedbackToasts({ onAnswer }: { onAnswer: (answer: Answer) => void }) {
const { toasts } = useToastManager();
return toasts.map((toastItem) => {
const asking = toastItem.type === "question";
return (
<Toast key={toastItem.id} toast={toastItem}>
<ToastContent className="gap-3 py-3 pr-3">
{asking ? null : (
<CircleCheckIcon
aria-hidden="true"
className="size-4 shrink-0 text-success"
/>
)}
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<ToastTitle />
<ToastDescription className="text-xs text-pretty" />
</div>
{asking ? (
<div className="flex shrink-0 items-center gap-1">
<Button
variant="outline"
size="icon-sm"
aria-label="Yes, it's resolved"
onClick={() => onAnswer("yes")}
>
<ThumbsUpIcon aria-hidden="true" />
</Button>
<Button
variant="outline"
size="icon-sm"
aria-label="No, I still need help"
onClick={() => onAnswer("no")}
>
<ThumbsDownIcon aria-hidden="true" />
</Button>
</div>
) : (
<ToastClose />
)}
</ToastContent>
</Toast>
);
});
}
export default function Toast04() {
const [status, setStatus] = React.useState<"open" | "pending" | Answer>(
"open",
);
const resolve = () => {
setStatus("pending");
toastManager.add({
id: FEEDBACK_ID,
type: "question",
title: "Did this solve your issue?",
description: `Your answer closes or reopens ticket #${ticket.id}.`,
timeout: 0,
// Swiping the prompt away without answering leaves the ticket open.
onClose: () =>
setStatus((current) => (current === "pending" ? "open" : current)),
});
};
const answer = (value: Answer) => {
setStatus(value);
toastManager.update(FEEDBACK_ID, {
type: "answered",
title: "Thanks for the feedback",
description:
value === "yes"
? `Ticket #${ticket.id} is closed. ${ticket.agent} will see your rating.`
: `Ticket #${ticket.id} is reopened. Expect a reply within 4 hours.`,
timeout: 4000,
});
};
const badge =
status === "yes"
? { label: "Closed", variant: "secondary" as const }
: status === "no"
? { label: "Reopened", variant: "destructive" as const }
: { label: "Awaiting you", variant: "outline" as const };
return (
<ToastProvider toastManager={toastManager}>
<div className="flex w-full max-w-sm flex-col gap-4 rounded-xl border border-border bg-card p-4 text-card-foreground">
<div className="flex items-start gap-3">
<span className="hidden size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground sm:flex">
<LifeBuoyIcon aria-hidden="true" className="size-4" />
</span>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="text-sm font-medium text-pretty">
{ticket.subject}
</span>
<span className="text-sm text-muted-foreground">
#{ticket.id} · {ticket.agent} replied 5 min ago
</span>
</div>
<Badge variant={badge.variant} className="shrink-0">
{badge.label}
</Badge>
</div>
<p className="text-sm text-pretty text-muted-foreground">
“I’ve refunded the second charge of $49.00. It should reach your card
in 3–5 business days.”
</p>
<Button
variant="outline"
disabled={status === "pending"}
onClick={resolve}
>
{status === "open" ? "Mark as resolved" : "Ask again"}
</Button>
</div>
<ToastPortal>
<ToastViewport>
<FeedbackToasts onAnswer={answer} />
</ToastViewport>
</ToastPortal>
</ToastProvider>
);
}
npx shadcn@latest add @sevenui/component/toast-04pnpm dlx shadcn@latest add @sevenui/component/toast-04yarn dlx shadcn@latest add @sevenui/component/toast-04bunx --bun shadcn@latest add @sevenui/component/toast-04"use client";
import * as React from "react";
import { BellIcon } from "lucide-react";
import { cn } from "cn";
import { Button } from "@/components/ui/button";
import {
Toast,
ToastClose,
ToastContent,
ToastDescription,
ToastPortal,
ToastProvider,
ToastTitle,
ToastViewport,
createToastManager,
useToastManager,
} from "@/components/ui/toast";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type Placement =
| "top-left"
| "top-center"
| "top-right"
| "bottom-left"
| "bottom-center"
| "bottom-right";
type SwipeDirection = "up" | "down" | "left" | "right";
const placements: { value: Placement; label: string }[] = [
{ value: "top-left", label: "Top left" },
{ value: "top-center", label: "Top center" },
{ value: "top-right", label: "Top right" },
{ value: "bottom-left", label: "Bottom left" },
{ value: "bottom-center", label: "Bottom center" },
{ value: "bottom-right", label: "Bottom right" },
];
const viewportClasses: Record<Placement, string> = {
"top-left": "top-4 bottom-auto sm:left-4 sm:right-auto",
"top-center": "top-4 bottom-auto sm:inset-x-0 sm:mx-auto",
"top-right": "top-4 bottom-auto",
"bottom-left": "sm:left-4 sm:right-auto",
"bottom-center": "sm:inset-x-0 sm:mx-auto",
"bottom-right": "",
};
// Mirrors the primitive's bottom-anchored stacking so toasts grow downward
// from the top edge and enter from above.
const topToastClasses = cn(
"top-0 bottom-auto origin-top after:top-auto after:bottom-full",
"[--offset-y:calc(var(--toast-offset-y)+calc(var(--toast-index)*var(--gap))+var(--toast-swipe-movement-y))]",
"[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-starting-style:[transform:translateY(-150%)]",
"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(-150%)]",
);
function getSwipeDirection(placement: Placement): SwipeDirection[] {
const vertical = placement.startsWith("top") ? "up" : "down";
if (placement.endsWith("left")) {
return [vertical, "left"];
}
if (placement.endsWith("right")) {
return [vertical, "right"];
}
return [vertical];
}
const toastManager = createToastManager();
function PlacedToasts({ placement }: { placement: Placement }) {
const { toasts } = useToastManager();
const isTop = placement.startsWith("top");
return toasts.map((toastItem) => (
<Toast
key={toastItem.id}
toast={toastItem}
swipeDirection={getSwipeDirection(placement)}
className={cn(isTop && topToastClasses)}
>
<ToastContent>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<ToastTitle />
<ToastDescription />
</div>
<ToastClose />
</ToastContent>
</Toast>
));
}
export default function Toast05() {
const [placement, setPlacement] = React.useState<Placement>("bottom-right");
const active = placements.find((item) => item.value === placement);
return (
<ToastProvider toastManager={toastManager}>
<div className="flex w-full max-w-xs flex-col gap-4">
<div className="flex flex-col gap-2">
<span id="toast-placement-label" className="text-sm font-medium">
Placement
</span>
<ToggleGroup
aria-labelledby="toast-placement-label"
value={[placement]}
onValueChange={(next) => {
if (next[0]) {
toastManager.close();
setPlacement(next[0] as Placement);
}
}}
className="grid aspect-video w-full grid-cols-3 grid-rows-2 gap-1.5 rounded-xl border border-border bg-muted/50 p-1.5"
>
{placements.map((item) => (
<ToggleGroupItem
key={item.value}
value={item.value}
aria-label={item.label}
className={cn(
"group/placement flex h-full w-full rounded-md p-2 hover:bg-background data-pressed:bg-background data-pressed:shadow-xs",
item.value.startsWith("top") ? "items-start" : "items-end",
item.value.endsWith("left") && "justify-start",
item.value.endsWith("center") && "justify-center",
item.value.endsWith("right") && "justify-end",
)}
>
<span
aria-hidden="true"
className="h-2 w-8 rounded-full bg-muted-foreground/30 group-data-pressed/placement:bg-primary"
/>
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<Button
variant="outline"
onClick={() =>
toastManager.add({
title: "Weekly report is ready",
description: `Showing at ${active?.label.toLowerCase()} of the screen.`,
})
}
>
<BellIcon aria-hidden="true" />
Show toast
</Button>
</div>
<ToastPortal>
<ToastViewport className={viewportClasses[placement]}>
<PlacedToasts placement={placement} />
</ToastViewport>
</ToastPortal>
</ToastProvider>
);
}
npx shadcn@latest add @sevenui/component/toast-05pnpm dlx shadcn@latest add @sevenui/component/toast-05yarn dlx shadcn@latest add @sevenui/component/toast-05bunx --bun shadcn@latest add @sevenui/component/toast-05"use client";
import * as React from "react";
import { CloudCheckIcon, PencilLineIcon, WifiOffIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch";
import {
Toast,
ToastClose,
ToastContent,
ToastDescription,
ToastPortal,
ToastProvider,
ToastTitle,
ToastViewport,
createToastManager,
useToastManager,
} from "@/components/ui/toast";
type ConnectionData = {
pending: number;
};
const CONNECTION_ID = "connection";
const toastManager = createToastManager<ConnectionData>();
function plural(count: number) {
return count === 1 ? "1 edit" : `${count} edits`;
}
function ConnectionIcon({ type }: { type: string | undefined }) {
if (type === "syncing") {
return <Spinner aria-hidden="true" className="size-4" />;
}
if (type === "online") {
return <CloudCheckIcon aria-hidden="true" className="size-4 text-success" />;
}
return <WifiOffIcon aria-hidden="true" className="size-4" />;
}
function ConnectionToasts() {
const { toasts } = useToastManager<ConnectionData>();
return toasts.map((toastItem) => {
const pending = toastItem.data?.pending ?? 0;
const offline = toastItem.type === "offline";
return (
<Toast key={toastItem.id} toast={toastItem}>
<ToastContent>
<span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
<ConnectionIcon type={toastItem.type} />
</span>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<ToastTitle />
<ToastDescription className="text-xs text-pretty" />
</div>
{offline && pending > 0 ? (
<Badge variant="secondary" className="shrink-0 tabular-nums">
{plural(pending)} waiting
</Badge>
) : null}
{toastItem.type === "online" ? <ToastClose /> : null}
</ToastContent>
</Toast>
);
});
}
export default function Toast06() {
const [online, setOnline] = React.useState(true);
const [pending, setPending] = React.useState(0);
const [saved, setSaved] = React.useState(12);
const syncTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
React.useEffect(
() => () => {
if (syncTimer.current) clearTimeout(syncTimer.current);
},
[],
);
const goOffline = () => {
if (syncTimer.current) clearTimeout(syncTimer.current);
setOnline(false);
toastManager.add({
id: CONNECTION_ID,
type: "offline",
priority: "high",
title: "You're offline",
description: "Edits are kept on this device until you reconnect.",
timeout: 0,
data: { pending },
});
};
const goOnline = () => {
setOnline(true);
const count = pending;
if (count === 0) {
toastManager.update(CONNECTION_ID, {
type: "online",
title: "Back online",
description: "Nothing to sync. You're up to date.",
timeout: 3000,
});
return;
}
toastManager.update(CONNECTION_ID, {
type: "syncing",
title: "Back online",
description: `Syncing ${plural(count)}…`,
});
syncTimer.current = setTimeout(() => {
setPending(0);
setSaved((current) => current + count);
toastManager.update(CONNECTION_ID, {
type: "online",
title: "All changes synced",
description: `${plural(count)} made offline are saved to the cloud.`,
timeout: 4000,
data: { pending: 0 },
});
}, 1200);
};
const edit = () => {
if (online) {
setSaved((current) => current + 1);
return;
}
const next = pending + 1;
setPending(next);
toastManager.update(CONNECTION_ID, { data: { pending: next } });
};
return (
<ToastProvider toastManager={toastManager}>
<div className="flex w-full max-w-sm flex-col gap-4 rounded-xl border border-border bg-card p-4 text-card-foreground">
<div className="flex items-start justify-between gap-3">
<div className="flex min-w-0 flex-col gap-0.5">
<span className="truncate text-sm font-medium">
Q4 hiring plan
</span>
<span className="text-sm text-muted-foreground" aria-live="polite">
{online
? `${saved} edits saved`
: `${plural(pending)} saved on this device`}
</span>
</div>
<Label className="shrink-0 gap-2 text-sm font-normal">
Online
<Switch
checked={online}
onCheckedChange={(checked) =>
checked ? goOnline() : goOffline()
}
/>
</Label>
</div>
<Button variant="outline" onClick={edit}>
<PencilLineIcon aria-hidden="true" />
Edit the document
</Button>
</div>
<ToastPortal>
<ToastViewport>
<ConnectionToasts />
</ToastViewport>
</ToastPortal>
</ToastProvider>
);
}
npx shadcn@latest add @sevenui/component/toast-06pnpm dlx shadcn@latest add @sevenui/component/toast-06yarn dlx shadcn@latest add @sevenui/component/toast-06bunx --bun shadcn@latest add @sevenui/component/toast-06"use client";
import * as React from "react";
import { ArchiveIcon, ArchiveRestoreIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Toast,
ToastAction,
ToastClose,
ToastContent,
ToastDescription,
ToastPortal,
ToastProvider,
ToastTitle,
ToastViewport,
createToastManager,
useToastManager,
} from "@/components/ui/toast";
type MeterData = {
duration: number;
};
const DURATION = 6000;
const toastManager = createToastManager<MeterData>();
type ToastObject = ReturnType<typeof useToastManager<MeterData>>["toasts"][number];
// The toast is added with `timeout: 0`, so the meter owns dismissal: the bar
// drains, pauses while the toast is hovered or focused, and closes the toast
// when it runs out. Bar and timer can never drift apart.
function MeterToast({ toastItem }: { toastItem: ToastObject }) {
const barRef = React.useRef<HTMLSpanElement>(null);
const animationRef = React.useRef<Animation | null>(null);
const duration = toastItem.data?.duration ?? DURATION;
React.useEffect(() => {
const bar = barRef.current;
if (!bar || typeof bar.animate !== "function") {
return;
}
const animation = bar.animate(
[{ transform: "scaleX(1)" }, { transform: "scaleX(0)" }],
{ duration, easing: "linear", fill: "forwards" },
);
animation.onfinish = () => toastManager.close(toastItem.id);
animationRef.current = animation;
return () => animation.cancel();
}, [duration, toastItem.id]);
const pause = () => animationRef.current?.pause();
const resume = (event: React.FocusEvent | React.PointerEvent) => {
const root = event.currentTarget;
if (root.matches(":hover") || root.contains(document.activeElement)) {
return;
}
animationRef.current?.play();
};
return (
<Toast
toast={toastItem}
className="overflow-hidden"
onPointerEnter={pause}
onPointerLeave={resume}
onFocus={pause}
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget)) {
resume(event);
}
}}
>
<ToastContent>
<ArchiveIcon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground"
/>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<ToastTitle />
<ToastDescription className="truncate" />
</div>
<ToastAction />
<ToastClose />
</ToastContent>
<span
aria-hidden="true"
className="absolute inset-x-0 bottom-0 h-0.5 bg-muted"
>
<span
ref={barRef}
className="block h-full origin-left bg-primary"
/>
</span>
</Toast>
);
}
function MeterToasts() {
const { toasts } = useToastManager<MeterData>();
return toasts.map((toastItem) => (
<MeterToast key={toastItem.id} toastItem={toastItem} />
));
}
export default function Toast07() {
const [archived, setArchived] = React.useState(false);
const restore = () => {
setArchived(false);
toastManager.close("archived-thread");
};
const archive = () => {
setArchived(true);
const id = toastManager.add({
id: "archived-thread",
title: "Conversation archived",
description: "“Q3 vendor review” moved to Archive.",
timeout: 0,
data: { duration: DURATION },
actionProps: {
children: "Undo",
onClick: () => {
setArchived(false);
toastManager.close(id);
},
},
});
};
return (
<ToastProvider toastManager={toastManager}>
<div className="flex w-full max-w-sm items-center gap-3 rounded-xl border border-border bg-card p-3 text-card-foreground">
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm font-medium">Q3 vendor review</span>
<span className="truncate text-sm text-muted-foreground">
{archived ? "Archived just now" : "Elena Park · 4 messages"}
</span>
</div>
<Button
variant="outline"
size="sm"
onClick={archived ? restore : archive}
>
{archived ? (
<ArchiveRestoreIcon aria-hidden="true" />
) : (
<ArchiveIcon aria-hidden="true" />
)}
{archived ? "Restore" : "Archive"}
</Button>
</div>
<ToastPortal>
<ToastViewport>
<MeterToasts />
</ToastViewport>
</ToastPortal>
</ToastProvider>
);
}
npx shadcn@latest add @sevenui/component/toast-07pnpm dlx shadcn@latest add @sevenui/component/toast-07yarn dlx shadcn@latest add @sevenui/component/toast-07bunx --bun shadcn@latest add @sevenui/component/toast-07"use client";
import * as React from "react";
import {
BellPlusIcon,
GitPullRequestIcon,
MessageSquareIcon,
RocketIcon,
ShieldAlertIcon,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Toast,
ToastClose,
ToastContent,
ToastDescription,
ToastPortal,
ToastProvider,
ToastTitle,
ToastViewport,
createToastManager,
useToastManager,
} from "@/components/ui/toast";
type Source = "deploy" | "review" | "comment" | "security";
type EventData = {
source: Source;
time: string;
};
const LIMIT = 3;
const sourceIcons: Record<Source, typeof RocketIcon> = {
deploy: RocketIcon,
review: GitPullRequestIcon,
comment: MessageSquareIcon,
security: ShieldAlertIcon,
};
const events: { title: string; description: string; data: EventData }[] = [
{
title: "Production deploy finished",
description: "web@4f2c9a1 is live in us-east and eu-west.",
data: { source: "deploy", time: "09:41" },
},
{
title: "Review requested",
description: "Sam Rivera asked you to review PR 1287: Billing webhooks.",
data: { source: "review", time: "09:42" },
},
{
title: "New comment on Pricing v3",
description: "“Let’s keep the annual toggle on by default.”",
data: { source: "comment", time: "09:44" },
},
{
title: "Dependency advisory",
description: "A moderate issue was reported in image-resize 2.1.",
data: { source: "security", time: "09:47" },
},
{
title: "Preview deploy ready",
description: "feat/checkout-redesign is available for QA.",
data: { source: "deploy", time: "09:52" },
},
];
const toastManager = createToastManager<EventData>();
function EventToasts() {
const { toasts } = useToastManager<EventData>();
return toasts.map((toastItem) => {
const Icon = sourceIcons[toastItem.data?.source ?? "deploy"];
return (
<Toast key={toastItem.id} toast={toastItem}>
<ToastContent className="items-start gap-3 p-3.5">
<span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-muted">
<Icon aria-hidden="true" className="size-4" />
</span>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex items-baseline justify-between gap-2">
<ToastTitle className="truncate" />
<time className="shrink-0 text-xs text-muted-foreground tabular-nums">
{toastItem.data?.time}
</time>
</div>
<ToastDescription className="line-clamp-2 text-pretty" />
</div>
<ToastClose className="-mt-1 -mr-1" />
</ToastContent>
</Toast>
);
});
}
function StackControls() {
const { toasts } = useToastManager<EventData>();
const [next, setNext] = React.useState(0);
const open = toasts.filter((item) => item.transitionStatus !== "ending");
const hidden = Math.max(0, open.length - LIMIT);
const push = () => {
const event = events[next];
toastManager.add({ ...event, timeout: 0 });
setNext((current) => (current + 1) % events.length);
};
return (
<div className="flex w-full max-w-sm flex-col gap-3 rounded-xl border border-border bg-card p-4 text-card-foreground">
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 flex-col">
<span className="text-sm font-medium">Notification stack</span>
<span className="text-sm text-muted-foreground">
Shows the newest {LIMIT}; older ones stay tucked away.
</span>
</div>
<div className="flex shrink-0 items-center gap-1.5" aria-live="polite">
<Badge variant="secondary" className="tabular-nums">
{open.length} open
</Badge>
{hidden > 0 && (
<Badge variant="outline" className="tabular-nums">
+{hidden} hidden
</Badge>
)}
</div>
</div>
<div className="flex flex-wrap gap-2">
<Button className="grow" onClick={push}>
<BellPlusIcon aria-hidden="true" />
Push notification
</Button>
<Button
variant="outline"
className="grow"
onClick={() => toastManager.close()}
disabled={open.length === 0}
>
Dismiss all
</Button>
</div>
</div>
);
}
export default function Toast08() {
return (
<ToastProvider toastManager={toastManager} limit={LIMIT}>
<StackControls />
<ToastPortal>
<ToastViewport>
<EventToasts />
</ToastViewport>
</ToastPortal>
</ToastProvider>
);
}
npx shadcn@latest add @sevenui/component/toast-08pnpm dlx shadcn@latest add @sevenui/component/toast-08yarn dlx shadcn@latest add @sevenui/component/toast-08bunx --bun shadcn@latest add @sevenui/component/toast-08"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Toaster, createToastManager } from "@/components/ui/toast";
const toastManager = createToastManager();
const preferences = [
{
id: "mentions",
label: "Mentions and replies",
description: "When someone @mentions you or replies to your comment.",
},
{
id: "assignments",
label: "Task assignments",
description: "When an issue is assigned to you or its due date moves.",
},
{
id: "digest",
label: "Weekly digest",
description: "A Monday summary of activity across your projects.",
},
] as const;
type PreferenceId = (typeof preferences)[number]["id"];
type PreferenceState = Record<PreferenceId, boolean>;
const initialState: PreferenceState = {
mentions: true,
assignments: true,
digest: false,
};
export default function Toast09() {
const [saved, setSaved] = React.useState<PreferenceState>(initialState);
const [draft, setDraft] = React.useState<PreferenceState>(initialState);
const [saving, setSaving] = React.useState(false);
const dirty = preferences.some(({ id }) => draft[id] !== saved[id]);
async function handleSave() {
setSaving(true);
const next = draft;
const enabled = preferences.filter(({ id }) => next[id]).length;
try {
await toastManager.promise(
new Promise<number>((resolve) => {
setTimeout(() => resolve(enabled), 1200);
}),
{
loading: {
title: "Saving preferences…",
description: "Syncing with your other devices.",
},
success: (count) => ({
title: "Email preferences saved",
description:
count === 0
? "You won't receive any email notifications."
: `You'll get email for ${count} of ${preferences.length} activity types.`,
}),
error: {
title: "Couldn't save preferences",
description: "Check your connection and try again.",
},
},
);
setSaved(next);
} finally {
setSaving(false);
}
}
return (
<>
<Toaster toastManager={toastManager} />
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Email notifications</CardTitle>
<CardDescription>
Choose which activity lands in your inbox.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col divide-y divide-border">
{preferences.map((preference) => (
<div
key={preference.id}
className="flex items-start justify-between gap-4 py-3 first:pt-0 last:pb-0"
>
<div className="flex min-w-0 flex-col gap-0.5">
<Label htmlFor={`toast-09-${preference.id}`}>
{preference.label}
</Label>
<p
id={`toast-09-${preference.id}-description`}
className="text-xs text-muted-foreground"
>
{preference.description}
</p>
</div>
<Switch
id={`toast-09-${preference.id}`}
aria-describedby={`toast-09-${preference.id}-description`}
checked={draft[preference.id]}
disabled={saving}
onCheckedChange={(checked) =>
setDraft((current) => ({
...current,
[preference.id]: checked,
}))
}
className="mt-0.5"
/>
</div>
))}
</CardContent>
<CardFooter className="justify-end gap-2">
<Button
variant="ghost"
disabled={!dirty || saving}
onClick={() => setDraft(saved)}
>
Discard
</Button>
<Button disabled={!dirty || saving} onClick={handleSave}>
{saving ? "Saving…" : "Save changes"}
</Button>
</CardFooter>
</Card>
</>
);
}
npx shadcn@latest add @sevenui/component/toast-09pnpm dlx shadcn@latest add @sevenui/component/toast-09yarn dlx shadcn@latest add @sevenui/component/toast-09bunx --bun shadcn@latest add @sevenui/component/toast-09"use client";
import * as React from "react";
import {
FileSpreadsheetIcon,
FileTextIcon,
FileVideoIcon,
ImageIcon,
RotateCcwIcon,
Trash2Icon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Toaster, createToastManager } from "@/components/ui/toast";
const toastManager = createToastManager();
const initialFiles = [
{
id: "q3-board-deck",
name: "Q3 board deck.pdf",
meta: "4.2 MB · Edited 2h ago",
icon: FileTextIcon,
},
{
id: "launch-budget",
name: "Launch budget.xlsx",
meta: "860 KB · Edited yesterday",
icon: FileSpreadsheetIcon,
},
{
id: "hero-shot",
name: "Hero shot final.png",
meta: "3.1 MB · Edited Sep 18",
icon: ImageIcon,
},
{
id: "onboarding-walkthrough",
name: "Onboarding walkthrough.mp4",
meta: "128 MB · Edited Sep 12",
icon: FileVideoIcon,
},
];
type FileEntry = (typeof initialFiles)[number];
export default function Toast10() {
const [files, setFiles] = React.useState<FileEntry[]>(initialFiles);
function restore(file: FileEntry) {
setFiles((current) => {
if (current.some((entry) => entry.id === file.id)) {
return current;
}
// Put the file back in its original position.
const order = initialFiles.map((entry) => entry.id);
return [...current, file].sort(
(a, b) => order.indexOf(a.id) - order.indexOf(b.id),
);
});
}
function remove(file: FileEntry) {
setFiles((current) => current.filter((entry) => entry.id !== file.id));
const toastId = toastManager.add({
title: "Moved to trash",
description: `${file.name} will be deleted permanently in 30 days.`,
timeout: 8000,
actionProps: {
children: (
<>
<RotateCcwIcon aria-hidden="true" />
Undo
</>
),
"aria-label": `Undo deleting ${file.name}`,
onClick: () => {
restore(file);
toastManager.close(toastId);
},
},
});
}
return (
<>
<Toaster toastManager={toastManager} />
<section
aria-labelledby="toast-10-heading"
className="w-full max-w-md overflow-hidden rounded-xl border bg-card text-card-foreground"
>
<header className="flex items-center justify-between gap-3 border-b px-4 py-3">
<h3 id="toast-10-heading" className="text-sm font-medium">
Marketing / Q3 launch
</h3>
<span className="text-xs text-muted-foreground tabular-nums">
{files.length} {files.length === 1 ? "file" : "files"}
</span>
</header>
{files.length > 0 ? (
<ul className="divide-y divide-border">
{files.map((file) => {
const Icon = file.icon;
return (
<li
key={file.id}
className="group flex items-center gap-3 px-4 py-2.5 hover:bg-muted/50"
>
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
<Icon className="size-4" aria-hidden="true" />
</span>
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm font-medium">
{file.name}
</span>
<span className="truncate text-xs text-muted-foreground">
{file.meta}
</span>
</div>
<Button
variant="ghost"
size="icon-sm"
aria-label={`Move ${file.name} to trash`}
className="text-muted-foreground hover:text-destructive"
onClick={() => remove(file)}
>
<Trash2Icon aria-hidden="true" />
</Button>
</li>
);
})}
</ul>
) : (
<div className="flex flex-col items-center gap-3 px-4 py-10 text-center">
<p className="text-sm text-muted-foreground">
This folder is empty.
</p>
<Button
variant="outline"
size="sm"
onClick={() => setFiles(initialFiles)}
>
<RotateCcwIcon aria-hidden="true" />
Restore all files
</Button>
</div>
)}
</section>
</>
);
}
npx shadcn@latest add @sevenui/component/toast-10pnpm dlx shadcn@latest add @sevenui/component/toast-10yarn dlx shadcn@latest add @sevenui/component/toast-10bunx --bun shadcn@latest add @sevenui/component/toast-10Oat heather
$118.00
"use client";
import * as React from "react";
import { CheckIcon, ShoppingBagIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Toast,
ToastAction,
ToastClose,
ToastContent,
ToastDescription,
ToastPortal,
ToastProvider,
ToastTitle,
ToastViewport,
createToastManager,
useToastManager,
} from "@/components/ui/toast";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type CartToastData = {
size: string;
quantity: number;
subtotal: string;
};
const toastManager = createToastManager<CartToastData>();
const product = {
name: "Merino crew sweater",
color: "Oat heather",
price: 118,
};
const sizes = ["XS", "S", "M", "L", "XL"];
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
function CartToasts() {
const { toasts } = useToastManager<CartToastData>();
return toasts.map((toastItem) => (
<Toast key={toastItem.id} toast={toastItem}>
<ToastContent className="items-start">
<img
src="/placeholder.svg"
alt=""
className="size-14 shrink-0 rounded-lg bg-muted object-cover"
/>
<div className="flex min-w-0 flex-1 flex-col gap-2">
<div className="flex flex-col gap-0.5">
<ToastTitle className="flex items-center gap-1.5">
<CheckIcon className="size-3.5 shrink-0" aria-hidden="true" />
<span className="truncate">{toastItem.title}</span>
</ToastTitle>
<ToastDescription className="text-xs" />
</div>
{toastItem.data ? (
<p className="text-xs text-muted-foreground tabular-nums">
Size {toastItem.data.size} · Qty {toastItem.data.quantity} ·{" "}
<span className="font-medium text-foreground">
{toastItem.data.subtotal}
</span>
</p>
) : null}
<div className="flex gap-2">
<ToastAction
render={<Button size="sm" />}
onClick={() => toastManager.close(toastItem.id)}
>
Checkout
</ToastAction>
<ToastClose
render={<Button variant="outline" size="sm" />}
aria-label="Keep shopping"
className="text-foreground"
>
Keep shopping
</ToastClose>
</div>
</div>
</ToastContent>
</Toast>
));
}
export default function Toast11() {
const [size, setSize] = React.useState("M");
const [cart, setCart] = React.useState<Record<string, number>>({});
const itemCount = Object.values(cart).reduce((sum, qty) => sum + qty, 0);
function addToCart() {
const quantity = (cart[size] ?? 0) + 1;
setCart((current) => ({ ...current, [size]: quantity }));
// Re-using the id updates the open toast instead of stacking duplicates.
toastManager.add({
id: `cart-${size}`,
title: "Added to your bag",
description: `${product.name} in ${product.color}`,
timeout: 6000,
data: {
size,
quantity,
subtotal: currency.format(product.price * quantity),
},
});
}
return (
<ToastProvider toastManager={toastManager}>
<ToastPortal>
<ToastViewport>
<CartToasts />
</ToastViewport>
</ToastPortal>
<article className="w-full max-w-sm overflow-hidden rounded-xl border bg-card text-card-foreground">
<div className="relative">
<img
src="/placeholder.svg"
alt={`${product.name} in ${product.color}`}
className="aspect-[4/3] w-full bg-muted object-cover"
/>
<span className="absolute top-3 right-3 flex items-center gap-1.5 rounded-full bg-background/90 px-2.5 py-1 text-xs font-medium tabular-nums shadow-sm">
<ShoppingBagIcon className="size-3.5" aria-hidden="true" />
<span className="sr-only">Items in bag:</span>
{itemCount}
</span>
</div>
<div className="flex flex-col gap-4 p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex flex-col gap-0.5">
<h3 className="font-medium">{product.name}</h3>
<p className="text-sm text-muted-foreground">{product.color}</p>
</div>
<p className="font-medium tabular-nums">
{currency.format(product.price)}
</p>
</div>
<div className="flex flex-col gap-2">
<span id="toast-11-size" className="text-sm font-medium">
Size
</span>
<ToggleGroup
aria-labelledby="toast-11-size"
variant="outline"
spacing={0}
value={[size]}
onValueChange={(next) => {
if (next.length > 0) setSize(next[0]);
}}
className="w-full"
>
{sizes.map((option) => (
<ToggleGroupItem
key={option}
value={option}
className="flex-1"
>
{option}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<Button size="lg" className="w-full" onClick={addToCart}>
<ShoppingBagIcon aria-hidden="true" />
Add to bag
</Button>
</div>
</article>
</ToastProvider>
);
}
npx shadcn@latest add @sevenui/component/toast-11pnpm dlx shadcn@latest add @sevenui/component/toast-11yarn dlx shadcn@latest add @sevenui/component/toast-11bunx --bun shadcn@latest add @sevenui/component/toast-11Send a signed test event to confirm an endpoint is reachable.
"use client";
import * as React from "react";
import { Loader2Icon, SendIcon, WebhookIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Toast,
ToastAction,
ToastClose,
ToastContent,
ToastDescription,
ToastPortal,
ToastProvider,
ToastTitle,
ToastViewport,
createToastManager,
useToastManager,
} from "@/components/ui/toast";
type DeliveryData = {
status: number;
latency: number;
event: string;
};
const toastManager = createToastManager<DeliveryData>();
const endpoints = [
{
id: "orders",
url: "https://api.northwind.dev/hooks/orders",
event: "order.paid",
status: 200,
latency: 184,
},
{
id: "fulfillment",
url: "https://ship.northwind.dev/v2/events",
event: "shipment.created",
status: 202,
latency: 412,
},
{
id: "legacy-crm",
url: "https://crm-legacy.northwind.dev/webhook",
event: "customer.updated",
status: 410,
latency: 96,
},
];
function DeliveryToasts() {
const { toasts } = useToastManager<DeliveryData>();
return toasts.map((toastItem) => {
const data = toastItem.data;
const failed = toastItem.type === "error";
return (
<Toast key={toastItem.id} toast={toastItem}>
<ToastContent className="items-start">
<div className="flex min-w-0 flex-1 flex-col gap-2">
<div className="flex items-center gap-2">
{toastItem.type === "loading" ? (
<Loader2Icon
className="size-4 shrink-0 animate-spin text-muted-foreground"
aria-hidden="true"
/>
) : null}
{data ? (
<Badge
variant={failed ? "destructive" : "secondary"}
className="font-mono tabular-nums"
>
{data.status}
</Badge>
) : null}
<ToastTitle className="truncate" />
</div>
<ToastDescription className="text-xs" />
{data ? (
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-xs">
<dt className="text-muted-foreground">Event</dt>
<dd className="truncate font-mono">{data.event}</dd>
<dt className="text-muted-foreground">Latency</dt>
<dd className="font-mono tabular-nums">{data.latency} ms</dd>
</dl>
) : null}
{toastItem.actionProps ? (
<ToastAction
className="w-fit"
onClick={() => toastManager.close(toastItem.id)}
/>
) : null}
</div>
<ToastClose className="-mt-1 -mr-1" />
</ToastContent>
</Toast>
);
});
}
export default function Toast12() {
const [pending, setPending] = React.useState<string | null>(null);
const [disabled, setDisabled] = React.useState<string[]>([]);
async function sendTest(endpoint: (typeof endpoints)[number]) {
setPending(endpoint.id);
const host = new URL(endpoint.url).host;
try {
await toastManager.promise(
new Promise<(typeof endpoints)[number]>((resolve, reject) => {
setTimeout(() => {
if (endpoint.status >= 400) {
reject(endpoint);
} else {
resolve(endpoint);
}
}, endpoint.latency + 600);
}),
{
loading: {
title: `Sending ${endpoint.event}`,
description: `POST to ${host}`,
},
success: (result) => ({
title: "Test event delivered",
description: `${host} acknowledged the payload.`,
data: {
status: result.status,
latency: result.latency,
event: result.event,
},
}),
error: (result: (typeof endpoints)[number]) => ({
title: "Endpoint rejected the event",
description: `${host} returned ${result.status}. Deliveries will keep failing until it's fixed.`,
priority: "high",
timeout: 0,
data: {
status: result.status,
latency: result.latency,
event: result.event,
},
actionProps: {
children: "Disable endpoint",
onClick: () =>
setDisabled((current) => [...current, result.id]),
},
}),
},
);
} catch {
// The toast already reports the failure.
} finally {
setPending(null);
}
}
return (
<ToastProvider toastManager={toastManager}>
<ToastPortal>
<ToastViewport>
<DeliveryToasts />
</ToastViewport>
</ToastPortal>
<section
aria-labelledby="toast-12-heading"
className="w-full max-w-lg overflow-hidden rounded-xl border bg-card text-card-foreground"
>
<header className="flex items-start gap-3 border-b px-4 py-3">
<WebhookIcon
className="mt-0.5 size-4 shrink-0 text-muted-foreground"
aria-hidden="true"
/>
<div className="flex flex-col gap-0.5">
<h3 id="toast-12-heading" className="text-sm font-medium">
Webhook endpoints
</h3>
<p className="text-xs text-muted-foreground">
Send a signed test event to confirm an endpoint is reachable.
</p>
</div>
</header>
<ul className="divide-y divide-border">
{endpoints.map((endpoint) => {
const isDisabled = disabled.includes(endpoint.id);
return (
<li
key={endpoint.id}
className="flex items-center gap-3 px-4 py-3"
>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span
className={
isDisabled
? "truncate font-mono text-xs text-muted-foreground line-through"
: "truncate font-mono text-xs"
}
>
{endpoint.url}
</span>
<span className="text-xs text-muted-foreground">
{isDisabled ? "Disabled · no events sent" : endpoint.event}
</span>
</div>
<Button
variant="outline"
size="sm"
disabled={pending !== null || isDisabled}
aria-label={`Send test event to ${endpoint.url}`}
onClick={() => sendTest(endpoint)}
>
{pending === endpoint.id ? (
<Loader2Icon className="animate-spin" aria-hidden="true" />
) : (
<SendIcon aria-hidden="true" />
)}
Test
</Button>
</li>
);
})}
</ul>
</section>
</ToastProvider>
);
}
npx shadcn@latest add @sevenui/component/toast-12pnpm dlx shadcn@latest add @sevenui/component/toast-12yarn dlx shadcn@latest add @sevenui/component/toast-12bunx --bun shadcn@latest add @sevenui/component/toast-12New members get access to every project in Acme Design.
"use client";
import * as React from "react";
import { MailIcon, UserPlusIcon } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
NativeSelect,
NativeSelectOption,
} from "@/components/ui/native-select";
import { Toaster, createToastManager } from "@/components/ui/toast";
const toastManager = createToastManager();
const roles = ["Viewer", "Editor", "Admin"];
const members = [
{ name: "Priya Natarajan", email: "priya@acme.io", role: "Owner" },
{ name: "Daniel Okafor", email: "daniel@acme.io", role: "Admin" },
];
type Invite = { email: string; role: string; sentAt: number };
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function initials(value: string) {
return value
.split(/[\s@.]+/)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase() ?? "")
.join("");
}
export default function Toast13() {
const [email, setEmail] = React.useState("");
const [role, setRole] = React.useState("Editor");
const [invites, setInvites] = React.useState<Invite[]>([
{ email: "marta@acme.io", role: "Viewer", sentAt: 0 },
]);
const [error, setError] = React.useState<string | null>(null);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const address = email.trim().toLowerCase();
// Format problems stay inline next to the field.
if (!emailPattern.test(address)) {
setError("Enter a full email address, like alex@acme.io.");
return;
}
setError(null);
// Outcomes of the request itself are reported with a toast.
const member = members.find((entry) => entry.email === address);
if (member) {
toastManager.add({
type: "info",
title: `${member.name} is already on the team`,
description: `They have ${member.role} access. Change roles from the members list.`,
});
return;
}
if (invites.some((invite) => invite.email === address)) {
toastManager.add({
type: "warning",
title: "Invite already pending",
description: `${address} hasn't accepted yet. Use Resend to nudge them.`,
});
return;
}
setInvites((current) => [
{ email: address, role, sentAt: Date.now() },
...current,
]);
setEmail("");
const toastId = toastManager.add({
type: "success",
title: `Invite sent to ${address}`,
description: `They'll join as ${role.toLowerCase()} once they accept. The link expires in 7 days.`,
actionProps: {
children: "Cancel invite",
onClick: () => {
setInvites((current) =>
current.filter((invite) => invite.email !== address),
);
toastManager.close(toastId);
toastManager.add({
title: "Invite canceled",
description: `The link sent to ${address} no longer works.`,
});
},
},
});
}
function resend(invite: Invite) {
const now = Date.now();
// Throttle resends so a teammate isn't spammed.
if (now - invite.sentAt < 60_000) {
toastManager.add({
id: `resend-${invite.email}`,
type: "warning",
title: "Hold on a minute",
description: `An invite was just sent to ${invite.email}. You can resend it in under a minute.`,
});
return;
}
setInvites((current) =>
current.map((entry) =>
entry.email === invite.email ? { ...entry, sentAt: now } : entry,
),
);
toastManager.add({
id: `resend-${invite.email}`,
type: "success",
title: "Invite resent",
description: `A fresh link is on its way to ${invite.email}.`,
});
}
return (
<>
<Toaster toastManager={toastManager} />
<section
aria-labelledby="toast-13-heading"
className="flex w-full max-w-md flex-col gap-5 rounded-xl border bg-card p-4 text-card-foreground"
>
<div className="flex flex-col gap-1">
<h3 id="toast-13-heading" className="font-medium">
Invite teammates
</h3>
<p className="text-sm text-muted-foreground">
New members get access to every project in Acme Design.
</p>
</div>
<form noValidate onSubmit={handleSubmit} className="flex flex-col gap-2">
<Label htmlFor="toast-13-email">Email address</Label>
<div className="flex flex-col gap-2 sm:flex-row">
<Input
id="toast-13-email"
type="email"
autoComplete="off"
placeholder="alex@acme.io"
value={email}
aria-invalid={error ? true : undefined}
aria-describedby={error ? "toast-13-error" : undefined}
onChange={(event) => {
setEmail(event.target.value);
if (error) setError(null);
}}
className="flex-1"
/>
<div className="flex gap-2">
<NativeSelect
aria-label="Role"
value={role}
onChange={(event) => setRole(event.target.value)}
className="flex-1 sm:flex-none"
>
{roles.map((option) => (
<NativeSelectOption key={option} value={option}>
{option}
</NativeSelectOption>
))}
</NativeSelect>
<Button type="submit">
<UserPlusIcon aria-hidden="true" />
Invite
</Button>
</div>
</div>
{error ? (
<p id="toast-13-error" className="text-xs text-destructive">
{error}
</p>
) : (
<p className="text-xs text-muted-foreground">
Try daniel@acme.io or marta@acme.io to see the other outcomes.
</p>
)}
</form>
<div className="flex flex-col gap-2">
<h4 className="text-xs font-medium text-muted-foreground">
Pending invites
</h4>
{invites.length > 0 ? (
<ul className="flex flex-col gap-1">
{invites.map((invite) => (
<li
key={invite.email}
className="flex items-center gap-3 rounded-lg py-1.5"
>
<Avatar size="sm">
<AvatarFallback className="text-[0.625rem]">
{initials(invite.email)}
</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm">{invite.email}</span>
<span className="text-xs text-muted-foreground">
{invite.role}
</span>
</div>
<Button
variant="ghost"
size="sm"
aria-label={`Resend invite to ${invite.email}`}
onClick={() => resend(invite)}
>
<MailIcon aria-hidden="true" />
Resend
</Button>
</li>
))}
</ul>
) : (
<p className="text-sm text-muted-foreground">
No pending invites.
</p>
)}
</div>
</section>
</>
);
}
npx shadcn@latest add @sevenui/component/toast-13pnpm dlx shadcn@latest add @sevenui/component/toast-13yarn dlx shadcn@latest add @sevenui/component/toast-13bunx --bun shadcn@latest add @sevenui/component/toast-134 files ready · 7.1 MB
"use client";
import * as React from "react";
import {
CircleCheckIcon,
CloudUploadIcon,
FileIcon,
UploadIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import {
Toast,
ToastAction,
ToastClose,
ToastContent,
ToastDescription,
ToastPortal,
ToastProvider,
ToastTitle,
ToastViewport,
createToastManager,
useToastManager,
} from "@/components/ui/toast";
type UploadData = {
progress: number;
current: string;
};
const toastManager = createToastManager<UploadData>();
const UPLOAD_TOAST_ID = "upload-batch";
const queue = [
{ name: "site-visit-001.jpg", size: 2.4 },
{ name: "site-visit-002.jpg", size: 3.1 },
{ name: "floor-plan-rev-c.pdf", size: 1.2 },
{ name: "structural-notes.docx", size: 0.4 },
];
const totalSize = queue.reduce((sum, file) => sum + file.size, 0);
function fileAt(progress: number) {
// Map overall progress to the file currently being sent.
let sent = 0;
for (const file of queue) {
sent += file.size;
if ((progress / 100) * totalSize < sent) return file;
}
return queue[queue.length - 1];
}
function UploadToasts() {
const { toasts } = useToastManager<UploadData>();
return toasts.map((toastItem) => {
const data = toastItem.data;
const done = toastItem.type === "success";
return (
<Toast key={toastItem.id} toast={toastItem}>
<ToastContent className="items-start">
<span className="mt-0.5 shrink-0 text-muted-foreground">
{done ? (
<CircleCheckIcon className="size-4" aria-hidden="true" />
) : (
<CloudUploadIcon className="size-4" aria-hidden="true" />
)}
</span>
<div className="flex min-w-0 flex-1 flex-col gap-2">
<div className="flex flex-col gap-0.5">
<ToastTitle />
<ToastDescription className="truncate text-xs" />
</div>
{data && !done ? (
<Progress
value={data.progress}
aria-label="Upload progress"
className="w-full"
/>
) : null}
{toastItem.actionProps ? (
<ToastAction className="w-fit" />
) : null}
</div>
{done ? <ToastClose className="-mt-1 -mr-1" /> : null}
</ToastContent>
</Toast>
);
});
}
export default function Toast14() {
const [status, setStatus] = React.useState<"idle" | "uploading" | "done">(
"idle",
);
const intervalRef = React.useRef<ReturnType<typeof setInterval> | null>(
null,
);
const stop = React.useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, []);
React.useEffect(() => stop, [stop]);
function cancel() {
stop();
setStatus("idle");
toastManager.update(UPLOAD_TOAST_ID, {
type: "info",
title: "Upload canceled",
description: "Files that finished stay in the folder.",
timeout: 4000,
data: undefined,
actionProps: undefined,
});
}
function start() {
stop();
setStatus("uploading");
let progress = 0;
toastManager.add({
id: UPLOAD_TOAST_ID,
type: "loading",
title: `Uploading ${queue.length} files`,
description: `${queue[0].name} · 0 of ${totalSize.toFixed(1)} MB`,
timeout: 0,
data: { progress: 0, current: queue[0].name },
actionProps: { children: "Cancel", onClick: cancel },
});
intervalRef.current = setInterval(() => {
progress = Math.min(100, progress + 4 + Math.random() * 6);
const file = fileAt(progress);
const sent = ((progress / 100) * totalSize).toFixed(1);
if (progress >= 100) {
stop();
setStatus("done");
toastManager.update(UPLOAD_TOAST_ID, {
type: "success",
title: `${queue.length} files uploaded`,
description: "Saved to Projects / Harbor Street renovation.",
timeout: 5000,
data: undefined,
actionProps: undefined,
});
return;
}
toastManager.update(UPLOAD_TOAST_ID, {
description: `${file.name} · ${sent} of ${totalSize.toFixed(1)} MB`,
data: { progress, current: file.name },
});
}, 350);
}
return (
<ToastProvider toastManager={toastManager}>
<ToastPortal>
<ToastViewport>
<UploadToasts />
</ToastViewport>
</ToastPortal>
<section
aria-labelledby="toast-14-heading"
className="flex w-full max-w-md flex-col gap-4 rounded-xl border bg-card p-4 text-card-foreground"
>
<div className="flex flex-col gap-0.5">
<h3 id="toast-14-heading" className="font-medium">
Harbor Street renovation
</h3>
<p className="text-sm text-muted-foreground">
{status === "done"
? "All files are in the project folder."
: `${queue.length} files ready · ${totalSize.toFixed(1)} MB`}
</p>
</div>
<ul className="flex flex-col gap-1 rounded-lg border border-dashed p-2">
{queue.map((file) => (
<li
key={file.name}
className="flex items-center gap-2.5 rounded-md px-2 py-1.5 text-sm"
>
{status === "done" ? (
<CircleCheckIcon
className="size-4 shrink-0 text-muted-foreground"
aria-hidden="true"
/>
) : (
<FileIcon
className="size-4 shrink-0 text-muted-foreground"
aria-hidden="true"
/>
)}
<span className="min-w-0 flex-1 truncate">{file.name}</span>
<span className="text-xs text-muted-foreground tabular-nums">
{file.size.toFixed(1)} MB
</span>
</li>
))}
</ul>
<Button
className="w-full"
disabled={status === "uploading"}
onClick={start}
>
<UploadIcon aria-hidden="true" />
{status === "uploading"
? "Uploading…"
: status === "done"
? "Upload again"
: "Upload all files"}
</Button>
</section>
</ToastProvider>
);
}
npx shadcn@latest add @sevenui/component/toast-14pnpm dlx shadcn@latest add @sevenui/component/toast-14yarn dlx shadcn@latest add @sevenui/component/toast-14bunx --bun shadcn@latest add @sevenui/component/toast-14"use client";
import * as React from "react";
import { BellOffIcon, MessageSquarePlusIcon } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Kbd } from "@/components/ui/kbd";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Toast,
ToastAction,
ToastClose,
ToastContent,
ToastDescription,
ToastPortal,
ToastProvider,
ToastTitle,
ToastViewport,
createToastManager,
useToastManager,
} from "@/components/ui/toast";
type MessageData = {
conversationId: string;
initials: string;
channel: string;
};
const toastManager = createToastManager<MessageData>();
type Conversation = {
id: string;
customer: string;
initials: string;
company: string;
channel: string;
preview: string;
time: string;
unread: boolean;
};
const initialConversations: Conversation[] = [
{
id: "c-2041",
customer: "Lena Hoffmann",
initials: "LH",
company: "Brightline Labs",
channel: "Live chat",
preview: "Thanks, the export works again!",
time: "9:12 AM",
unread: false,
},
{
id: "c-2043",
customer: "Marcus Bell",
initials: "MB",
company: "Fieldnote",
channel: "Email",
preview: "Can we move our renewal call to Friday?",
time: "8:47 AM",
unread: false,
},
];
// Messages that arrive while the demo runs, oldest first.
const incoming = [
{
id: "c-2044",
customer: "Aiko Tanaka",
initials: "AT",
company: "Parcel & Co",
channel: "Live chat",
preview: "Our checkout is returning a 502 since the last deploy.",
},
{
id: "c-2045",
customer: "Rafael Souza",
initials: "RS",
company: "Tidewater",
channel: "Email",
preview: "Is SSO included in the Growth plan or only Enterprise?",
},
{
id: "c-2046",
customer: "Grace Liu",
initials: "GL",
company: "Northwind",
channel: "Live chat",
preview: "Could you resend the invoice for September?",
},
];
function MessageToasts({ onOpen }: { onOpen: (id: string) => void }) {
const { toasts } = useToastManager<MessageData>();
return toasts.map((toastItem) => (
<Toast key={toastItem.id} toast={toastItem}>
<ToastContent className="items-start">
<Avatar>
<AvatarFallback className="text-xs">
{toastItem.data?.initials}
</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col gap-2">
<div className="flex min-w-0 flex-col gap-0.5">
<ToastTitle className="truncate" />
<ToastDescription className="line-clamp-2" />
</div>
<div className="flex items-center gap-2">
<ToastAction
render={<Button size="sm" />}
onClick={() => {
if (toastItem.data) onOpen(toastItem.data.conversationId);
toastManager.close(toastItem.id);
}}
>
Reply
</ToastAction>
<span className="text-xs text-muted-foreground">
{toastItem.data?.channel}
</span>
</div>
</div>
<ToastClose className="-mt-1 -mr-1" />
</ToastContent>
</Toast>
));
}
export default function Toast15() {
const [conversations, setConversations] =
React.useState<Conversation[]>(initialConversations);
const [nextIndex, setNextIndex] = React.useState(0);
const [activeId, setActiveId] = React.useState(initialConversations[0].id);
const [quiet, setQuiet] = React.useState(false);
const unreadCount = conversations.filter((item) => item.unread).length;
const active = conversations.find((item) => item.id === activeId);
const nextMessage = incoming[nextIndex];
const open = React.useCallback((id: string) => {
setActiveId(id);
setConversations((current) =>
current.map((item) =>
item.id === id ? { ...item, unread: false } : item,
),
);
}, []);
function receive() {
if (!nextMessage) return;
const time = new Date().toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
});
setConversations((current) => [
{ ...nextMessage, time, unread: true },
...current,
]);
setNextIndex((index) => index + 1);
// Focus mode still records the message, it just skips the interruption.
if (quiet) return;
toastManager.add({
id: nextMessage.id,
title: `${nextMessage.customer} · ${nextMessage.company}`,
description: nextMessage.preview,
timeout: 7000,
data: {
conversationId: nextMessage.id,
initials: nextMessage.initials,
channel: nextMessage.channel,
},
});
}
return (
<ToastProvider toastManager={toastManager}>
<ToastPortal>
<ToastViewport>
<MessageToasts onOpen={open} />
</ToastViewport>
</ToastPortal>
<section
aria-labelledby="toast-15-heading"
className="w-full max-w-md overflow-hidden rounded-xl border bg-card text-card-foreground"
>
<header className="flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3">
<div className="flex items-center gap-2">
<h3 id="toast-15-heading" className="text-sm font-medium">
Support inbox
</h3>
{unreadCount > 0 ? (
<Badge className="tabular-nums">{unreadCount} unread</Badge>
) : null}
</div>
<Label className="gap-2 text-xs font-normal text-muted-foreground">
<BellOffIcon className="size-3.5" aria-hidden="true" />
Focus mode
<Switch
size="sm"
checked={quiet}
onCheckedChange={(checked) => setQuiet(checked)}
/>
</Label>
</header>
<ul aria-label="Conversations" className="divide-y divide-border">
{conversations.map((item) => {
const isActive = item.id === activeId;
return (
<li key={item.id}>
<button
type="button"
aria-current={isActive ? "true" : undefined}
onClick={() => open(item.id)}
className="flex w-full items-start gap-3 px-4 py-3 text-left outline-none hover:bg-muted/50 focus-visible:bg-muted/50 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset aria-[current=true]:bg-muted"
>
<Avatar size="sm" className="mt-0.5">
<AvatarFallback className="text-[0.625rem]">
{item.initials}
</AvatarFallback>
</Avatar>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="flex items-center justify-between gap-2">
<span
className={
item.unread
? "truncate text-sm font-semibold"
: "truncate text-sm font-medium"
}
>
{item.customer}
</span>
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">
{item.time}
</span>
</span>
<span className="truncate text-xs text-muted-foreground">
{item.preview}
</span>
</span>
{item.unread ? (
<span className="mt-1.5 size-2 shrink-0 rounded-full bg-primary">
<span className="sr-only">Unread</span>
</span>
) : null}
</button>
</li>
);
})}
</ul>
<footer className="flex flex-col gap-3 border-t bg-muted/50 px-4 py-3">
<p className="text-xs text-muted-foreground" aria-live="polite">
{active
? `Viewing ${active.customer} (${active.company}) via ${active.channel.toLowerCase()}.`
: "Select a conversation."}
</p>
<div className="flex flex-wrap items-center justify-between gap-2">
<Button
variant="outline"
size="sm"
disabled={!nextMessage}
onClick={receive}
>
<MessageSquarePlusIcon aria-hidden="true" />
{nextMessage ? "Simulate new message" : "Queue is empty"}
</Button>
<span className="hidden items-center gap-1.5 text-xs text-muted-foreground sm:flex">
<Kbd>F6</Kbd> jumps to notifications
</span>
</div>
</footer>
</section>
</ToastProvider>
);
}
npx shadcn@latest add @sevenui/component/toast-15pnpm dlx shadcn@latest add @sevenui/component/toast-15yarn dlx shadcn@latest add @sevenui/component/toast-15bunx --bun shadcn@latest add @sevenui/component/toast-15