Shadcn Toast for React and Tailwind
Provides feedback on an action.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/toast.jsonpnpm dlx shadcn@latest add https://kit.dev/r/toast.jsonnpx shadcn@latest add https://kit.dev/r/toast.jsonyarn shadcn@latest add https://kit.dev/r/toast.json<Step>This component depends on Button, and Spinner. Install them first if you haven't already.</Step>
Install the following dependencies:
bun add @ark-ui/react lucide-react tailwind-variantspnpm add @ark-ui/react lucide-react tailwind-variantsnpm install @ark-ui/react lucide-react tailwind-variantsyarn add @ark-ui/react lucide-react tailwind-variantsImport the following variables into your CSS file
@theme inline {
--color-destructive-foreground: var(--destructive-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
}
:root {
--destructive-foreground: var(--color-red-700);
--info: var(--color-blue-500);
--info-foreground: var(--color-blue-700);
--success: var(--color-emerald-500);
--success-foreground: var(--color-emerald-700);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-700);
}
.dark {
--destructive-foreground: var(--color-red-400);
--info: var(--color-blue-500);
--info-foreground: var(--color-blue-400);
--success: var(--color-emerald-500);
--success-foreground: var(--color-emerald-400);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-400);
}Copy and paste the following code into your project.
"use client";
import { Portal } from "@ark-ui/react/portal";
import {
Toast as ArkToast,
Toaster as ArkToaster,
createToaster,
useToastContext,
} from "@ark-ui/react/toast";
import {
CircleAlertIcon,
CircleCheckIcon,
InfoIcon,
TriangleAlertIcon,
XIcon,
} from "lucide-react";
import type React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
export { createToaster } from "@ark-ui/react/toast";
export const useToast = useToastContext;
export const toast = createToaster({
max: 3,
overlap: true,
placement: "bottom-end",
});
interface ToasterProps
extends Omit<
React.ComponentProps<typeof ArkToaster>,
"toaster" | "children"
> {
/**
* Toaster instance
*/
toaster?: React.ComponentProps<typeof ArkToaster>["toaster"];
}
export const Toaster = (props: ToasterProps) => {
const { toaster: toasterInstance = toast, className, style, ...rest } = props;
return (
<Portal>
<ArkToaster
className={cn(
"w-[calc(100%-var(--viewport-offset-left))] sm:w-(--width)",
"data-[align=center]:left-[calc(var(--viewport-offset-right)/2)]!",
"sm:data-[align=center]:w-full",
className
)}
data-slot="toaster"
style={{ "--width": "356px", ...style } as React.CSSProperties}
toaster={toasterInstance}
{...rest}
>
{(toastItem) => <ToastItem toast={toastItem} />}
</ArkToaster>
</Portal>
);
};
const TOAST_ICONS = {
error: <CircleAlertIcon />,
info: <InfoIcon />,
loading: <Spinner />,
success: <CircleCheckIcon />,
warning: <TriangleAlertIcon />,
} as const;
interface ToastItemProps extends React.ComponentProps<typeof ArkToast.Root> {
/**
* The toast item data
*/
toast: ArkToast.Options;
}
export const ToastItem = (props: ToastItemProps) => {
const { toast: toastData, className, ...rest } = props;
const ToastIcon = toastData.type
? TOAST_ICONS[toastData.type as keyof typeof TOAST_ICONS]
: null;
const isExplicitClosable = toastData.closable === false;
return (
<ArkToast.Root
className={cn(
"z-(--z-index) translate-x-(--x) translate-y-(--y)",
"relative",
"w-[calc(100%-var(--viewport-offset-left))] sm:w-(--width)",
"px-3.5 py-3",
"flex items-start justify-between gap-1.5",
"bg-popover",
"select-none text-card-foreground text-sm",
"rounded-lg border shadow-lg/5",
"scale-(--scale) opacity-(--opacity)",
"transition-all duration-250 will-change-[translate,opacity,scale]",
"ease-[cubic-bezier(0.21,1.02,0.73,1)]",
"data-[state=closed]:transition-[translate,scale,opacity]",
"data-[state=closed]:duration-[300ms,300ms,150ms]",
"data-[state=closed]:ease-[cubic-bezier(0.06,0.71,0.55,1)]",
"motion-reduce:transition-none!",
className
)}
data-slot="toast"
{...rest}
>
<div className="flex items-start gap-1.5">
<div
className={cn(
"in-data-[type=warning]:text-warning",
"in-data-[type=success]:text-success",
"in-data-[type=error]:text-destructive",
"in-data-[type=info]:text-info",
"[&_svg]:pointer-events-none [&_svg]:h-lh [&_svg]:w-4 [&_svg]:shrink-0"
)}
data-slot="toast-icon"
>
{ToastIcon}
</div>
<div className="flex flex-col gap-0.5">
<ArkToast.Title
className="font-medium text-sm"
data-slot="toast-title"
>
{toastData.title}
</ArkToast.Title>
{!!toastData.description && (
<ArkToast.Description
className="text-muted-foreground text-sm"
data-slot="toast-description"
>
{toastData.description}
</ArkToast.Description>
)}
</div>
</div>
<div className="flex items-center gap-2">
{!!toastData.action && (
<ArkToast.ActionTrigger
asChild
data-slot="toast-action-trigger"
onClick={toastData.action.onClick}
>
<Button size="sm" variant="secondary">
{toastData.action.label}
</Button>
</ArkToast.ActionTrigger>
)}
{!isExplicitClosable && (
<ArkToast.CloseTrigger asChild data-slot="toast-close-trigger">
<Button
aria-label="Close"
className="opacity-64 hover:opacity-100"
size="icon-xs"
variant="ghost"
>
<XIcon />
</Button>
</ArkToast.CloseTrigger>
)}
</div>
</ArkToast.Root>
);
};Update the import paths to match your project setup.
Usage
import { Toaster } from "@/components/ui/toast";const toaster = useToast();
// add the toaster to the layout
<Toaster toaster={toaster} />
// show a toast
toaster.create({
title: "Show Info Toast",
description: "This is an info toast",
type: "info",
});Deduplication
Toasts with the same id are deduplicated. If no id is passed, a new toast is created.
Placement
By default, the toaster is placed at the bottom right corner of the screen.
Update the placement option in createToaster() to customize the toaster position.
const baseToaster = createToaster({
placement: "bottom-end",
overlap: true,
max: 4,
});Max toasts
Any extra toasts will be queued and rendered when a toast has been dismissed.
By default, the toaster displays up to 3 toasts. Update the max option in createToaster() to change the maximum number of visible toasts.
const baseToaster = createToaster({
placement: "bottom-end",
overlap: true,
max: 4,
});Examples
Variants
Use success, error, warning, and info for the matching icon and accent color.
You can also pass type on toast.create.
Promise
Action
Duration
Control how long a toast stays visible with the duration option.
Use Infinity to keep the toast visible until the user dismisses it.
Not Closable
Use closable prop to control whether the close button is shown.
API Reference
Toaster
Toast container. Place once in your app layout.
| Prop | Type | Default |
|---|---|---|
toaster | ReturnType<typeof createToaster> | - |
className | string | - |
toast
Toast instance. Used to create and manage toasts.
| Prop | Type | Default |
|---|---|---|
title | string | - |
description | string | - |
type | "success" | "error" | "warning" | "info" | "loading" | "info" |
duration | number | 3000 |
closable | boolean | true |
For a complete list of props, see the Ark UI documentation.