Toast
Generates toast notifications.
Installation
pnpm dlx shadcn@latest add @lumi-ui/toast
Toaster component once in your root layout.export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html>
<body>
<div className="root">
{children}
<Toaster
position="bottom-right"
swipeDirection={["right", "down"]}
limit={3}
/>
</div>
</body>
</html>
);
}@theme block of your global CSS file. /* Spacing around window */
--toast-viewport-padding: 2rem;
/* Toast width */
--toast-width: 22rem;
/* Spacing between toasts */
--toast-gap: 0.75rem;
/* Spacing between stacked toast */
--toast-peek: 0.75rem;
/* Mobile vs Desktop Widths */
--toast-max-width-mobile: 80%;
--toast-max-width-desktop: var(--toast-width);/*
Toast Viewport
*/
@layer utilities {
.toast-viewport {
@apply fixed flex flex-col w-[var(--toast-max-width-mobile)] lg:w-[var(--toast-max-width-desktop)] outline-none pointer-events-none;
}
/* Viewport Positioning */
.toast-viewport[data-position^="top"] { top: var(--toast-viewport-padding); bottom: auto; }
.toast-viewport[data-position^="bottom"] { bottom: var(--toast-viewport-padding); top: auto; }
.toast-viewport[data-position$="left"] { left: var(--toast-viewport-padding); right: auto; }
.toast-viewport[data-position$="right"] { right: var(--toast-viewport-padding); left: auto; }
.toast-viewport[data-position$="center"] {
@apply left-1/2 right-auto -translate-x-1/2;
}
}
/*
Toast Root
*/
@layer utilities {
.toast-root {
@apply absolute w-full pointer-events-auto;
/* Stacking */
--toast-index: 0; /* Default fallback */
z-index: calc(1000 - var(--toast-index));
/* Transitions */
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1),
height 0.15s ease-out;
/* Height Calculation */
height: var(--toast-frontmost-height, var(--toast-height));
/* Common Ghost Element (for hit areas) */
&::after {
content: "";
position: absolute;
left: 0;
width: 100%;
height: calc(var(--toast-gap) + 1px);
background: transparent;
}
/* Math for collapsed stacking (shrinking effect) */
--toast-scale: calc(max(0, 1 - (var(--toast-index) * 0.1)));
--toast-shrink: calc(1 - var(--toast-scale));
--toast-height-approx: var(--toast-frontmost-height, var(--toast-height));
}
/*
Positions
*/
/* Bottom */
.toast-root[data-position^="bottom"] {
@apply bottom-0 top-auto;
transform-origin: bottom center;
/* Direction Multipliers: Bottom is "Positive" direction for entering from below */
--dir-y: 1;
/* Calculate Offsets */
--collapsed-offset-y: calc(
(var(--toast-index) * var(--toast-peek) * -1) - /* Stack peek upwards */
(var(--toast-shrink) * var(--toast-height-approx)) /* Shrink upwards */
);
--expanded-offset-y: calc(
(var(--toast-offset-y) * -1) - /* Move up based on total stack height */
(var(--toast-index) * var(--toast-gap))
);
/* Default Transform: Collapsed + Swipe Handling */
transform:
translateX(var(--toast-swipe-movement-x, 0px))
translateY(calc(var(--toast-swipe-movement-y, 0px) + var(--collapsed-offset-y)))
scale(var(--toast-scale));
}
/* Top */
.toast-root[data-position^="top"] {
@apply top-0 bottom-auto;
transform-origin: top center;
/* Direction Multipliers: Top is "Negative" direction for entering from above */
--dir-y: -1;
/* Calculate Offsets (Signs flipped from Bottom) */
--collapsed-offset-y: calc(
(var(--toast-index) * var(--toast-peek)) + /* Stack peek downwards */
(var(--toast-shrink) * var(--toast-height-approx)) /* Shrink downwards */
);
--expanded-offset-y: calc(
var(--toast-offset-y) +
(var(--toast-index) * var(--toast-gap))
);
/* Default Transform: Collapsed + Swipe Handling */
transform:
translateX(var(--toast-swipe-movement-x, 0px))
translateY(calc(var(--toast-swipe-movement-y, 0px) + var(--collapsed-offset-y)))
scale(var(--toast-scale));
}
/*
State Modifiers
*/
/* Expanded State */
.toast-root[data-expanded] {
height: var(--toast-height);
transform:
translateX(var(--toast-swipe-movement-x, 0px))
translateY(var(--expanded-offset-y))
scale(1);
}
/* Entering Animation */
.toast-root[data-starting-style] {
opacity: 0;
transform: translateY(calc(var(--dir-y) * 150%));
}
/* Exiting Animation */
.toast-root[data-ending-style] {
opacity: 0;
/* Default Exit */
&:not([data-swipe-direction]) {
transform: translateY(calc(var(--dir-y) * 150%));
}
/* Swipe Up/Down */
&[data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y) - 150%));
}
&[data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y) + 150%));
}
/* Swipe Left/Right */
&[data-swipe-direction="left"] {
transform:
translateX(calc(var(--toast-swipe-movement-x) - 150%))
translateY(var(--expanded-offset-y));
}
&[data-swipe-direction="right"] {
transform:
translateX(calc(var(--toast-swipe-movement-x) + 150%))
translateY(var(--expanded-offset-y));
}
}
/* Limited State */
.toast-root[data-limited] {
opacity: 0;
transform: scale(0.9);
}
}toast object to trigger notifications from anywhere in your app.import { toast } from "@/components/ui/toast"
toast.add({ title: "Event Created", description: "Monday at 9:00am" })
// Semantic Variants
toast.success({ title: "Success", description: "Your event has been created" })
toast.error({ title: "Error occurred", description: "Please try again" })Features
Toast variants
Closable
Promise
Varying heights
Cookbook
Undo
Anchored toasts
Updating toasts
Custom content
Custom anchored toasts
Customization
Since you own the code, you can tweak the internal logic to fit your design system.
Styling (Variants)
We use cva to handle variants. Modify the toastVariants definition to change colors or borders.
const toastVariants = cva(
"rounded-sm outline ...", // Base styles
{
variants: {
type: {
// Add new types or modify existing color schemes here
purple: "bg-purple-50 text-purple-900 border-purple-200",
}
}
}
)Icons
The icons are mapped in the Icons constant. You can swap these for a different icon library or custom SVGs.
const Icons = {
success: CheckCircle2, // Replace with your own icon
error: AlertCircle,
// ...
};Layout Structure
If you want to change the layout (e.g., move the action button below the text instead of to the right), modify the JSX inside StackedToast -> ToastContent.
<div className="flex flex-col gap-2"> {/* Changed to column */}
<ToastTitle>{toast.title}</ToastTitle>
{/* ... */}
</div>Anchored Toasts
The toast.anchor() method is useful for "Copied!" tooltips or context-specific errors. It uses a separate BaseToast.Provider internally. You can customize the AnchoredToast component if you need different layout or use toast.anchor to create a unique design that fits your use case.
Animation
Animations are handled via CSS/Tailwind classes. By default, we support all toast positions and directions of swipe dismiss. Feel free to modify the CSS to fit your design system.
AnchoredToast uses utilities classes animate-popup for animations. Replace it with the following snippet in BaseToast.Root if you choose to opt out:
<BaseToast.Root
data-slot="toast-root"
toast={toast}
className={cn(
!isCustomContent && toastVariants({ type: "default" }),
"data-[starting-style]:opacity-0 data-[starting-style]:scale-95 ",
"data-[ending-style]:opacity-0 data-[ending-style]:scale-95 ",
"transition-all duration-200"
)}
>API Reference
Toaster
The provider component that renders the viewport.
toast
The main interface for managing toasts. Extends Base UI's useToastManager.
options
Extends Base UI's Method options with custom data fields.