Toast
Opinionated layouts for toasts that carry more than one line of text: media, title and description, actions under the copy, and a list of secondary actions behind a disclosure.
Installation
Usage
import {
showToast, Toast, ToastRow, ToastMedia,
ToastContent, ToastTitle, ToastDescription,
ToastActions, ToastAction, ToastDisclosure, ToastClose,
ToastPanel, ToastSection, ToastOption,
} from "@/components/ui/toast"Examples
Default
Actions sit under the text, not beside it, so they keep their full label. Pick a cell to move where toasts land.
position="bottom-right"
Secondary actions behind a disclosure
The one or two actions worth interrupting for stay in the toast; the rest go in ToastPanel, three or four at most. Opening the panel cancels the dismiss timer for good, since a countdown under a list of choices fails WCAG 2.2.1 (Timing Adjustable). Move the toast to a top cell to watch the panel open downward.
position="bottom-right"
Grouped actions, wider column
Add label to ToastSection only when the actions sort into distinct jobs, as they do here; most panels are one flat list and want no headings. Omit dismissAfter and the toast waits until it is acted on, and raising width on the Toaster lifts the cap it can grow toward.
position="bottom-right"
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| showToast(render, options) | (id) => ReactElement, { dismissAfter?, ...ExternalToast } | - | Fires the toast. Named showToast rather than toast so it never collides with sonner's own toast(). Wraps toast.custom(), pins sonner's duration to Infinity so the card owns its dismiss timer, and gives sonner's list item the width floor it otherwise lacks. |
| dismissAfter | number | - | On showToast options. Milliseconds before the toast auto-dismisses. Pauses on hover, on focus, and while the tab is hidden. Omit to keep the toast up until it is acted on. |
| position | "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" | "bottom-right" | On <Toaster />, alongside width. A card grows away from the edge it is anchored to, so a bottom toast opens its panel upward and a top toast opens downward. No change is needed on the card itself. |
| width | number | string | 400 | On <Toaster />. A ceiling, not a fixed width: a toast sizes to its own content between a 300px floor and this cap, so a one-line confirmation stays small. Both bounds live on the list, not the card, since sonner positions each toast absolutely inside the column. |
| expanded | boolean | - | On Toast. Controlled disclosure state. Pair with onExpandedChange; leave both off for uncontrolled. Worth controlling when several toasts can stack at once: sonner measures a toast's height only when its JSX identity changes, so re-firing showToast with the same id on expand is what keeps the toasts behind it correctly offset. Passing it WITHOUT onExpandedChange pins the panel open, which is the decision-card shape described in the notes: drop the ToastDisclosure, add a ToastClose, and expect no dismiss timer. Escape then dismisses the card rather than collapsing it, since there is nothing to collapse back to. A pinned card is also the one case the stacking caveat above does not apply to, because it is born at full height and sonner measures it correctly on the first pass. |
| label | string | "Notification options" | On Toast. Accessible name applied once the panel is open, when the card reads as a group of controls rather than a status line. |
| label | ReactNode | - | On ToastSection. Optional, and usually left off: most panels are one flat list of three or four actions. Add it only when the actions sort into distinct jobs, in which case it names the group for screen readers as well as sighted users. |
| variant / size | ButtonProps | - | On ToastAction and ToastDisclosure. Both render a kit Button and pass these straight through. The action defaults to default, the disclosure to ghost, and both sit at sm, one step below the button default. ToastOption takes neither: it is a menu-item row, not a button. |
| dismissOnClick | boolean | true | On ToastAction and ToastOption. Dismisses the toast after the handler runs. |
"use client"
import * as React from "react"
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
import { ChevronDownIcon, XIcon } from "lucide-react"
import { toast, type ExternalToast } from "sonner"
import { cn } from "@/registry/lib/utils"
import { Button } from "@/registry/ui/button"
import { CollapsibleContent } from "@/registry/ui/collapsible"
/**
* Opinionated bodies for `sonner` toasts that carry more than one line of text.
*
* Three rules from the toast accessibility literature are built into the parts
* rather than left to the docs, because getting them wrong is invisible until
* an audit:
*
* 1. A toast may carry ONE optional action (Undo, Retry). Anything the user has
* to decide between belongs behind a disclosure, so the common path stays a
* real toast. `ToastPanel` is that disclosure.
*
* THE EXCEPTION, and it is a real one: when the choices ARE the message
* rather than an elaboration of it. If the toast exists to capture a
* decision, and saying nothing is a worse outcome than the interruption,
* then hiding the choices behind a click suppresses the only thing the card
* was for. Pin the panel with `expanded` and render no `ToastDisclosure`.
* Two obligations come with that: supply a `ToastClose` (rule 2 leaves the
* card unable to expire) and be sure this really is a decision card, because
* the disclosure is right for every toast whose choices are extras.
* 2. Auto-dismiss plus an action the user must reach is a WCAG 2.2.1 (Timing
* Adjustable, Level A) failure. Opening the panel therefore cancels the
* dismiss timer permanently: once there is a decision on screen, the toast
* waits. A panel pinned open by rule 1 is open from mount, so such a card
* never had a timer to begin with and only leaves when it is answered or
* closed.
* 3. Nothing here takes focus on its own. Consensus is against focus-stealing
* toasts; sonner already ships the alternative, a global alt+T that moves
* focus into the toast region.
*
* `toast.custom()` renders its JSX with `data-styled="false"`, so sonner
* contributes no padding, background, border, or width. Every surface decision
* below is this component's, not sonner's.
*/
// The timer lives here, not in sonner's `duration`, because it has to be
// cancellable from inside the tree when the panel opens. `showToast()` pins
// sonner's own duration to Infinity so the two never race.
type DismissContextValue = {
id: string | number
dismissAfter?: number
}
const DismissContext = React.createContext<DismissContextValue | null>(null)
/**
* Fires a `Toast` body. Wraps `toast.custom()` and pins sonner's duration
* to Infinity so the card owns its own dismiss timer (see rule 2 above).
* Returns the toast id.
*/
// rsc-client-only: showToast. Imperative, pushes into sonner's live store from
// an event handler. There is no server render in which calling it would mean
// anything, so it stays in this client module by design.
function showToast(
render: (id: string | number) => React.ReactElement,
options?: Omit<ExternalToast, "duration"> & {
/** Milliseconds before the toast auto-dismisses. Pauses on hover and on
* focus, and is cancelled outright when a `ToastPanel` opens. Omit to
* keep the toast up until it is acted on. */
dismissAfter?: number
}
) {
const { dismissAfter, className, ...rest } = options ?? {}
return toast.custom(
(id) => (
<DismissContext.Provider value={{ id, dismissAfter }}>
{render(id)}
</DismissContext.Provider>
),
{
...rest,
// Sonner only sizes its list item under `[data-sonner-toast][data-styled=true]`,
// and a custom toast is always `data-styled="false"`. That leaves the item
// with no width at all, which is most of what we want: an absolutely
// positioned box with `width: auto` shrinks to fit, so each toast comes out
// the size of its own message. What it lacks is a floor, so a two-word
// confirmation collapses to the width of the words.
//
// A floor is all this adds. The ceiling is already the Toaster's `width`,
// because shrink-to-fit can never exceed the containing block, and sonner's
// own mobile rule is a two-attribute selector that outranks this class, so
// full-bleed below 600px still wins.
className: cn("min-w-75", className),
duration: Infinity,
}
)
}
type ToastContextValue = {
/** Dismisses the toast this card is rendering inside, if there is one. */
dismiss: () => void
/** Whether a `ToastPanel` is present, so the disclosure trigger only
* renders when there is something to disclose. Read from the children at
* render time rather than reported up by an effect: an effect would leave the
* trigger missing on the first paint and pop it in a frame later, which is a
* layout shift in the one place a toast cannot afford one. */
hasPanel: boolean
}
const ToastContext = React.createContext<ToastContextValue | null>(null)
function useToastContext(part: string) {
const context = React.useContext(ToastContext)
if (!context) {
throw new Error(`${part} must be rendered inside a Toast.`)
}
return context
}
type ToastProps = Omit<
React.ComponentProps<typeof CollapsiblePrimitive.Root>,
"open" | "defaultOpen" | "onOpenChange" | "title"
> & {
/** Controlled disclosure state for `ToastPanel`. Passing `expanded` with no
* `onExpandedChange` PINS the panel open: the disclosure becomes unnecessary,
* the dismiss timer never runs, and Escape dismisses the card instead of
* collapsing it. That is the decision-card shape from rule 1, and it needs a
* `ToastClose`. */
expanded?: boolean
defaultExpanded?: boolean
onExpandedChange?: (expanded: boolean) => void
/** Accessible name for the card once its panel is open and it reads as a
* group of choices rather than a status message. */
label?: string
}
function Toast({
className,
expanded,
defaultExpanded = false,
onExpandedChange,
label = "Notification options",
children,
...props
}: ToastProps) {
const dismissContext = React.useContext(DismissContext)
const [internalExpanded, setInternalExpanded] = React.useState(defaultExpanded)
const rootRef = React.useRef<HTMLDivElement>(null)
// Direct children only, which is the documented structure. Keeping this
// synchronous is the point: the disclosure trigger has to be there on the
// first paint, not one render later.
const hasPanel = React.Children.toArray(children).some(
(child) => React.isValidElement(child) && child.type === ToastPanel
)
const isExpanded = expanded ?? internalExpanded
const setExpanded = React.useCallback(
(next: boolean) => {
if (expanded === undefined) setInternalExpanded(next)
onExpandedChange?.(next)
},
[expanded, onExpandedChange]
)
const dismiss = React.useCallback(() => {
if (dismissContext) toast.dismiss(dismissContext.id)
}, [dismissContext])
// Auto-dismiss. Deliberately skipped once the panel is open: a countdown
// running under a set of choices is the WCAG 2.2.1 failure this component
// exists to prevent. Pauses while the pointer is over the card or focus is
// inside it, and while the tab is hidden, so a backgrounded toast does not
// expire unseen.
const dismissAfter = dismissContext?.dismissAfter
React.useEffect(() => {
if (dismissAfter == null || isExpanded) return
const node = rootRef.current
if (!node) return
let remaining = dismissAfter
let startedAt = 0
let timer: number | undefined
const resume = () => {
if (timer !== undefined || remaining <= 0) return
startedAt = performance.now()
timer = window.setTimeout(dismiss, remaining)
}
const pause = () => {
if (timer === undefined) return
window.clearTimeout(timer)
timer = undefined
remaining -= performance.now() - startedAt
}
const onVisibilityChange = () => {
if (document.visibilityState === "hidden") pause()
else resume()
}
if (document.visibilityState === "visible") resume()
node.addEventListener("pointerenter", pause)
node.addEventListener("pointerleave", resume)
node.addEventListener("focusin", pause)
node.addEventListener("focusout", resume)
document.addEventListener("visibilitychange", onVisibilityChange)
return () => {
pause()
node.removeEventListener("pointerenter", pause)
node.removeEventListener("pointerleave", resume)
node.removeEventListener("focusin", pause)
node.removeEventListener("focusout", resume)
document.removeEventListener("visibilitychange", onVisibilityChange)
}
}, [dismissAfter, isExpanded, dismiss])
// While the toast list is hovered, sonner pins an explicit height on its list
// item, taken from a measurement made before the panel existed. It only
// remeasures when a toast's JSX identity changes, which opening a panel from
// inside does not do. The item is anchored to the edge the toasts come from,
// so the disclosed content runs past that edge and off the screen: on a
// bottom-anchored toaster, straight off the bottom of the viewport.
//
// Releasing the height lets the item size to the card, so the card grows away
// from the anchored edge the way it should.
//
// Latched rather than tied to the open state: restoring the pinned height the
// moment the panel starts closing snaps the item back to its stale size while
// the panel is still easing shut, which is a visible jump at the exact point
// the eye is following the motion. Once a card has been opened it is the toast
// being dealt with, so it keeps its natural height for the rest of its life.
//
// Latched during render, not in an effect. The effect version set state after
// paint, so the first expanded frame still rendered with the pinned height
// and the release landed a frame late. React's sanctioned "adjust state while
// rendering" pattern re-runs this component immediately, before anything is
// committed, so the height is already free on the frame the panel opens.
const [wasExpanded, setWasExpanded] = React.useState(false)
if (isExpanded && !wasExpanded) setWasExpanded(true)
React.useEffect(() => {
if (!wasExpanded) return
// `closest`, not `parentElement`: sonner nests custom JSX two wrappers deep
// (the list item holds a [data-content] div holding a [data-title] div), so
// the pinned height is three levels up, not one.
const item = rootRef.current?.closest<HTMLElement>("[data-sonner-toast]")
if (!item) return
item.style.height = "auto"
return () => {
item.style.height = ""
}
}, [wasExpanded])
const context = React.useMemo<ToastContextValue>(
() => ({ dismiss, hasPanel }),
[dismiss, hasPanel]
)
// A panel pinned open by the caller (`expanded` with no `onExpandedChange`)
// cannot be closed, so asking it to collapse is a no-op. Detected rather than
// attempted, because the failure is silent: the handler below would swallow
// the key and then do nothing.
const panelIsPinned = expanded !== undefined && onExpandedChange === undefined
// Escape closes the disclosure rather than the whole toast: the user opened a
// set of choices, and backing out of them should not also throw away the Undo
// sitting above them.
//
// When the panel is pinned there is no disclosure to back out of, and the card
// has no timer either (see rule 2), so Escape has to escalate to dismissing the
// whole toast. Otherwise the one key every overlay answers to would be the one
// key this card ignores, on the only variant that never leaves on its own.
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key !== "Escape" || !isExpanded) return
event.stopPropagation()
if (panelIsPinned) dismiss()
else setExpanded(false)
}
return (
<ToastContext.Provider value={context}>
<CollapsiblePrimitive.Root
ref={rootRef}
open={isExpanded}
onOpenChange={setExpanded}
onKeyDown={onKeyDown}
data-slot="toast"
// Overlay tier: rounded-md, and the edge is the ring folded into the
// shadow stack rather than a border. Width comes from the Toaster's
// `--width`; a card wider than its toaster would overflow sonner's
// absolutely positioned list item, so w-full is the only safe answer.
className={cn(
"bg-popover text-popover-foreground w-full overflow-hidden rounded-md shadow-ring-md",
className
)}
// Once choices are on screen the card is a group of controls, not a
// status line, and needs a name of its own.
{...(isExpanded ? { role: "group", "aria-label": label } : {})}
{...props}
>
{children}
</CollapsiblePrimitive.Root>
</ToastContext.Provider>
)
}
/** The primary block: media, text, and the actions beneath them. Stays put when
* the panel below it opens, so nothing the user is looking at moves. */
function ToastRow({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="toast-row"
// items-start, not center: the media and the close button align to the
// first line of text rather than floating to the middle of a block whose
// height now depends on how many actions it carries.
className={cn("flex items-start gap-3 px-4 py-4", className)}
{...props}
/>
)
}
/** Leading slot for an icon, avatar, or thumbnail. */
function ToastMedia({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="toast-media"
className={cn(
// pt-0.5 centres a 16px glyph on the title's 19px first line, so the
// alignment is right by default instead of every caller having to nudge
// it. Scoped icon sizing, like the kit's other icon slots, so a caller
// passing an explicit size wins instead of fighting this rule.
"flex shrink-0 items-center justify-center pt-0.5 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
/** Wraps title, description, and actions so they stack as one block. Actions
* live in here rather than beside the text so they align to the text column
* with or without media, without the card having to know which. */
function ToastContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="toast-content"
className={cn("flex min-w-0 flex-1 flex-col gap-1", className)}
{...props}
/>
)
}
function ToastTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="toast-title"
// CardTitle's recipe (leading-snug, semibold, balanced wrap) at the toast
// tier's size, with the description one step below it on the type scale
// exactly as a Card pairs them.
className={cn("text-sm leading-snug font-semibold text-balance", className)}
{...props}
/>
)
}
function ToastDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="toast-description"
className={cn(
"text-muted-foreground text-1xs text-pretty",
className
)}
{...props}
/>
)
}
/** The action row, beneath the text rather than beside it. Buttons under the
* copy they belong to read in the order they are meant to be used, and they
* keep their full label instead of being squeezed by the text next to them. */
function ToastActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="toast-actions"
// mt-2 opens the text-to-action gap past the 4px holding title and
// description together, so the buttons read as a separate band.
className={cn("mt-2 flex flex-wrap items-center gap-2", className)}
{...props}
/>
)
}
/** An action (Undo, Retry, Delete). A plain kit `Button`, so it takes the same
* `variant` and `size` as every other button in the system: `default` for the
* one thing you want pressed, `subtle` beside it, `destructive` when the action
* destroys something. Dismisses the toast after running, since acting on a
* toast is the end of it. */
function ToastAction({
size = "sm",
onClick,
dismissOnClick = true,
...props
}: React.ComponentProps<typeof Button> & { dismissOnClick?: boolean }) {
const { dismiss } = useToastContext("ToastAction")
return (
<Button
data-slot="toast-action"
// One step down from the button default. A toast is chrome over the top of
// whatever the user is actually doing, so its controls sit below the
// weight of the controls in the page behind it.
size={size}
onClick={(event) => {
onClick?.(event)
if (dismissOnClick) dismiss()
}}
{...props}
/>
)
}
/** Opens the choices below. Rendered only when a `ToastPanel` is present, so
* a plain toast never grows an affordance that leads nowhere. Renders as a kit
* `Button` so it sits in the action row as a peer, not as a stray text link. */
function ToastDisclosure({
variant = "ghost",
size = "sm",
children = "Why?",
...props
}: Omit<React.ComponentProps<typeof CollapsiblePrimitive.Trigger>, "render"> &
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
const { hasPanel } = useToastContext("ToastDisclosure")
if (!hasPanel) return null
return (
<CollapsiblePrimitive.Trigger
data-slot="toast-disclosure"
render={
<Button variant={variant} size={size} className="group/disclosure" />
}
{...props}
>
{children}
{/* A toggle has to show which way it is pointing. Same recipe as the
* kit's CollapsibleTrigger chevron, so the two disclosures in the system
* turn at the same speed on the same curve. */}
<ChevronDownIcon className="transition-transform duration-200 ease-out motion-reduce:transition-none group-data-[panel-open]/disclosure:rotate-180" />
</CollapsiblePrimitive.Trigger>
)
}
/** Icon close.
*
* REQUIRED on any card that cannot expire: one fired without `dismissAfter`, or
* one whose panel is pinned open, which cancels the timer (see rule 2). Without
* it such a card has no exit that is not answering it, and a notification the
* user cannot decline is not a notification.
*
* Optional everywhere else, since a toast that times out already leaves on its
* own. The trigger is whether the card can go away by itself, NOT whether it
* carries an action: a persistent card with an Undo on it still needs a way to
* leave without pressing Undo. */
function ToastClose({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { dismiss } = useToastContext("ToastClose")
return (
<Button
data-slot="toast-close"
variant="ghost"
size="icon-sm"
aria-label="Dismiss notification"
// The outdent seats the GLYPH on the px-4 rail; its hover box is allowed
// to run into the padding beside it. -mt-0.5 does the same job vertically:
// the 24px box is taller than the title's 19px line, so it has to come up
// to sit centred on it, the mirror of the media slot coming down.
className={cn("tap-target text-subtle-foreground -mt-0.5 -mr-1.5 shrink-0", className)}
onClick={(event) => {
onClick?.(event)
dismiss()
}}
{...props}
>
<XIcon />
</Button>
)
}
/** The disclosed region. Animates its own height (the kit's Collapsible panel),
* so the row above it never moves and the card grows from the bottom. */
function ToastPanel({
className,
...props
}: React.ComponentProps<typeof CollapsibleContent>) {
return (
<CollapsibleContent
data-slot="toast-panel"
// Internal divider stays a real border; the ring is only ever the outer
// edge of the card.
//
// The panel owns the whole region's inset, exactly as CardContent does,
// so its top and bottom are one value instead of two rules that have to
// be kept in agreement. px-4 py-4 matches the row above it, which is what
// makes the card read as one rhythm rather than two stacked boxes. Sections
// inside carry no padding of their own; they only space themselves apart.
className={cn("border-border border-t px-4 py-4", className)}
{...props}
/>
)
}
/** A labelled group of choices. The label names the group for screen readers as
* well as sighted users, so "Not interested" is never heard on its own. */
function ToastSection({
className,
label,
children,
...props
}: Omit<React.ComponentProps<"div">, "role"> & { label?: React.ReactNode }) {
const labelId = React.useId()
return (
<div
data-slot="toast-section"
// Spacing between sections only. The panel owns the region's inset, so a
// section never adds an edge of its own, which is what kept the panel's
// top and bottom out of step before.
className={cn("not-first:pt-4", className)}
{...(label ? { role: "group", "aria-labelledby": labelId } : {})}
{...props}
>
{label && (
<div
id={labelId}
data-slot="toast-section-label"
// The kit's group-heading recipe, shared with CommandGroup and
// DropdownMenuLabel. Uppercase and letter-spacing belong to TableHead,
// which is a column header in a data grid, not a label above a short
// list of choices in an overlay.
//
// No inset of its own: a heading is text, so it sits on the card's
// rail with the title and description. Only the rows below it are
// controls, and only they carry the extra inset their own fill needs.
// Space below only; the panel and the section already own above.
className="text-muted-foreground pb-2 text-xs font-medium"
>
{label}
</div>
)}
{/* The rows carry a resting fill, so they need a gap to read as separate
* targets. One step, not two: at 8px the list starts to look like
* stacked cards rather than one group. */}
<div className="flex flex-col gap-1">{children}</div>
</div>
)
}
/** One secondary action, on the design system's menu-item tier
* (`px-2 py-1.5`, the same tier Dropdown, Select, Command, and Combobox items
* use). It keeps the tier's `rounded-sm` where those items step down to
* `rounded-xs`: they sit in a `p-1` popup, so their radius has to go concentric
* with a corner 4px away, while this list sits 16px inside the toast's `px-4`
* and its corners never interact with the card's.
*
* It carries a resting tint, which a menu item does not. A menu item can afford
* to be transparent because you opened the menu, so you already know everything
* in it is selectable. This list just appears under a message, with no such
* context, and a row of plain text does not read as something you can press.
* The tint is the signifier; the accent fill on hover and on keyboard focus is
* the same "this one" feedback `data-highlighted` gives in the kit's menus.
*
* Not a `Button`: these are secondary to the actions above them, and button
* weight on every row would outrank the primary action. */
function ToastOption({
className,
onClick,
dismissOnClick = true,
...props
}: React.ComponentProps<"button"> & { dismissOnClick?: boolean }) {
const { dismiss } = useToastContext("ToastOption")
return (
<button
type="button"
data-slot="toast-option"
// No transition on the highlight, matching the kit's other menu items: a
// fill that chases the cursor down a list reads as lag, not as polish.
className={cn(
"relative flex w-full cursor-pointer items-center gap-2 rounded-sm bg-neutral-500/10 px-2 py-1.5 pointer-coarse:py-3 text-left text-sm outline-none select-none hover:bg-accent-100 dark:hover:bg-accent-900 hover:text-accent focus-visible:bg-accent-100 dark:focus-visible:bg-accent-900 focus-visible:text-accent disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3 [&_svg:not([class*='text-'])]:text-muted-foreground hover:[&_svg:not([class*='text-'])]:text-accent focus-visible:[&_svg:not([class*='text-'])]:text-accent",
className
)}
onClick={(event) => {
onClick?.(event)
if (dismissOnClick) dismiss()
}}
{...props}
/>
)
}
export {
showToast,
Toast,
ToastRow,
ToastMedia,
ToastContent,
ToastTitle,
ToastDescription,
ToastActions,
ToastAction,
ToastDisclosure,
ToastClose,
ToastPanel,
ToastSection,
ToastOption,
}