Component

Composable toast notifications with stacking, swipe-to-dismiss, and an imperative manager API aligned with Base UI patterns. Wrap your app in Toast.Provider, render a portal + viewport, and map active toasts to Toast.Root.

preview

Click repeatedly to stack. Hover the stack to expand every toast.

React
'use client' import { useEffect, useMemo } from 'react' import Toast, { createToastManager } from '@radui/ui/Toast' import type { ToastData } from '@radui/ui/Toast' import Button from '@radui/ui/Button' function Toaster() { const { toasts } = Toast.useToastManager() return ( <Toast.Portal> <Toast.Viewport> {toasts.map((t: ToastData) => ( <Toast.Root key={t.id} toast={t}> <Toast.Content> <Toast.Title>{t.title}</Toast.Title> {t.description ? ( <Toast.Description>{t.description}</Toast.Description> ) : null} {t.actionProps != null ? <Toast.Action /> : null} <Toast.Close /> </Toast.Content> </Toast.Root> ))} </Toast.Viewport> </Toast.Portal> ) } function StackingInner() { const manager = Toast.useToastManager() useEffect(() => () => { manager.dismissAll() }, [manager]) return ( <div className="mx-auto flex w-full max-w-xl flex-col gap-3"> <Toaster /> <p className="text-sm text-gray-800"> Click repeatedly to stack. Hover the stack to expand every toast. </p> <div className="flex flex-wrap gap-2"> <Button type="button" onClick={() => manager.add({ title: 'Changes saved.' })}> Default </Button> <Button type="button" onClick={() => manager.add({ title: 'File uploaded.', variant: 'success' })}> Success </Button> <Button type="button" onClick={() => manager.add({ title: 'Something went wrong.', variant: 'error' })}> Error </Button> </div> </div> ) } export default function ToastStackingExample() { // Each example owns its queue; the default singleton would show every toast in every example on the page. const toastManager = useMemo(() => createToastManager(), []) return ( <Toast.Provider toastManager={toastManager} position="bottom-right" gap={14} limit={3}> <StackingInner /> </Toast.Provider> ) }

Import all parts of the component and piece them together

React
import Toast from '@radui/ui/Toast' export default () => { return ( <Toast.Provider> <Toast.Portal> <Toast.Viewport> <Toast.Root toast={{ id: 'example' }}> <Toast.Content> <Toast.Title /> <Toast.Description /> <Toast.Action /> <Toast.Close /> </Toast.Content> </Toast.Root> </Toast.Viewport> </Toast.Portal> </Toast.Provider> ) }

Wraps your app (or a subtree) and owns toast state, stacking, and the active toast manager.

PropTypeDefault
children
ReactNode
--
customRootClass
string
''
position
string
bottom-right
expand
boolean
false
gap
number
14
limit
number
3
maxToasts
number
--
timeout
number
5000
toastManager
ToastManager
--

Renders toast children into the theme portal root (or document.body).

PropTypeDefault
children
ReactNode
--
container
HTMLElement | null
theme portal / body

The notifications region (<ol>) that measures heights, handles hover expand, and positions the stack.

PropTypeDefault
children
ReactNode
--
className
string
''

One toast item (<li>): timers, swipe-to-dismiss, stack transforms, and live region semantics.

PropTypeDefault
toast
ToastData
--
children
ReactNode
--
className
string
''

Card wrapper around the toast body; forwards standard div attributes.

PropTypeDefault
className
string
''
children
ReactNode
--
…props
HTMLAttributes<HTMLDivElement>
--

Primary line of the toast; rendered as a <div> (not a heading) so transient notifications do not add to the page outline.

PropTypeDefault
className
string
''
children
ReactNode
--
…props
HTMLAttributes<HTMLHeadingElement>
--

Secondary copy for the toast; optional when only a title is shown.

PropTypeDefault
className
string
''
children
ReactNode
--
…props
HTMLAttributes<HTMLParagraphElement>
--

Optional action button; toast.actionProps are merged first, then your props.

PropTypeDefault
type
string
button
className
string
''
children
ReactNode
--
…props
ButtonHTMLAttributes<HTMLButtonElement>
--

Dismiss control; calls the item dismiss handler after any onClick you provide.

PropTypeDefault
className
string
''
children
ReactNode
Γ—
onClick
function
--
…props
ButtonHTMLAttributes<HTMLButtonElement>
--

Accessibility

ShortcutDescription
Tab

Moves focus through any interactive content inside visible toasts, including action and close buttons. Focusing the viewport expands the stack.

Shift + Tab

Moves focus to the previous focusable control and keeps the stack expanded while focus remains inside the viewport.

Enter

Activates the focused Toast.Action or Toast.Close button.

Space

Activates the focused Toast.Action or Toast.Close button.

ReferenceWhat Rad UI follows

Each Toast.Root uses aria-live with aria-atomic so assistive technologies announce notification updates. High-priority toasts announce assertively; other toasts announce politely.

Toast.Action and Toast.Close render native buttons, so activation follows the standard button keyboard model.

Toast.Viewport exposes the notifications stack as a labelled region while preserving normal focus movement through interactive toast controls.

Examples

preview

position on Toast.Provider moves the viewport to each corner or edge.

React
'use client' import { useEffect, useMemo, useState } from 'react' import Toast, { createToastManager } from '@radui/ui/Toast' import type { ToastData } from '@radui/ui/Toast' import Button from '@radui/ui/Button' const POSITIONS = [ 'top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right', ] as const type Corner = (typeof POSITIONS)[number] function Toaster() { const { toasts } = Toast.useToastManager() return ( <Toast.Portal> <Toast.Viewport> {toasts.map((t: ToastData) => ( <Toast.Root key={t.id} toast={t}> <Toast.Content> <Toast.Title>{t.title}</Toast.Title> <Toast.Close /> </Toast.Content> </Toast.Root> ))} </Toast.Viewport> </Toast.Portal> ) } function PositionsInner({ position, setPosition, }: { position: Corner setPosition: (p: Corner) => void }) { const manager = Toast.useToastManager() useEffect(() => () => { manager.dismissAll() }, [manager]) return ( <div className="flex w-full max-w-xl flex-col gap-3"> <Toaster /> <p className="text-sm text-gray-800"> <code className="text-xs">position</code> on{' '} <code className="text-xs">Toast.Provider</code> moves the viewport to each corner or edge. </p> <div className="flex flex-wrap gap-2"> {POSITIONS.map((p) => ( <Button key={p} type="button" color={p === position ? 'green' : undefined} onClick={() => { setPosition(p) manager.add({ title: `Position: ${p}` }) }}> {p.replace('-', ' ')} </Button> ))} </div> </div> ) } export default function ToastPositionsExample() { // Each example owns its queue; the default singleton would show every toast in every example on the page. const toastManager = useMemo(() => createToastManager(), []) const [position, setPosition] = useState<Corner>('bottom-right') return ( <Toast.Provider toastManager={toastManager} key={position} position={position} gap={12} limit={2}> <PositionsInner position={position} setPosition={setPosition} /> </Toast.Provider> ) }
preview

useToastManager().promise() shows a loading toast, then resolves to success or error.

React
'use client' import { useEffect, useMemo } from 'react' import Toast, { createToastManager } from '@radui/ui/Toast' import type { ToastData } from '@radui/ui/Toast' import Button from '@radui/ui/Button' function Toaster() { const { toasts } = Toast.useToastManager() return ( <Toast.Portal> <Toast.Viewport> {toasts.map((t: ToastData) => ( <Toast.Root key={t.id} toast={t}> <Toast.Content> <Toast.Title>{t.title}</Toast.Title> {t.description ? ( <Toast.Description>{t.description}</Toast.Description> ) : null} <Toast.Close /> </Toast.Content> </Toast.Root> ))} </Toast.Viewport> </Toast.Portal> ) } function PromiseInner() { const manager = Toast.useToastManager() useEffect(() => () => { manager.dismissAll() }, [manager]) function runPromise() { manager.promise( new Promise<string>((resolve, reject) => { const ok = Math.random() > 0.35 setTimeout(() => { if (ok) resolve('Done') else reject(new Error('Failed')) }, 1800) }), { loading: 'Working…', success: (data: string) => `Success: ${data}`, error: (err: unknown) => `Error: ${err instanceof Error ? err.message : String(err)}`, }, ) } return ( <div className="flex w-full max-w-xl flex-col gap-3"> <Toaster /> <p className="text-sm text-gray-800"> <code className="text-xs">useToastManager().promise()</code> shows a loading toast, then resolves to success or error. </p> <Button type="button" onClick={runPromise}> Run async action </Button> </div> ) } export default function ToastPromiseExample() { // Each example owns its queue; the default singleton would show every toast in every example on the page. const toastManager = useMemo(() => createToastManager(), []) return ( <Toast.Provider toastManager={toastManager} position="bottom-right" gap={14} limit={3}> <PromiseInner /> </Toast.Provider> ) }
preview

Pass toastManager={createToastManager()} to keep notifications separate from the default singleton.

React
'use client' import { useEffect, useMemo } from 'react' import Toast, { createToastManager } from '@radui/ui/Toast' import type { ToastData } from '@radui/ui/Toast' import Button from '@radui/ui/Button' function Toaster() { const { toasts } = Toast.useToastManager() return ( <Toast.Portal> <Toast.Viewport> {toasts.map((t: ToastData) => ( <Toast.Root key={t.id} toast={t}> <Toast.Content> <Toast.Title>{t.title}</Toast.Title> <Toast.Close /> </Toast.Content> </Toast.Root> ))} </Toast.Viewport> </Toast.Portal> ) } function IsolatedInner() { const manager = Toast.useToastManager() return ( <> <Toaster /> <Button type="button" onClick={() => manager.add({ title: 'Uses an isolated queue', variant: 'info' })}> Add (isolated manager) </Button> </> ) } export default function ToastIsolatedManagerExample() { const tm = useMemo(() => createToastManager({ timeout: 4000 }), []) useEffect(() => () => { tm.close() }, [tm]) return ( <div className="flex w-full max-w-xl flex-col gap-3"> <p className="text-sm text-gray-800"> Pass <code className="text-xs">toastManager=&#123;createToastManager()&#125;</code> to keep notifications separate from the default singleton. </p> <Toast.Provider toastManager={tm} position="bottom-right" limit={2}> <IsolatedInner /> </Toast.Provider> </div> ) }
preview

Set actionProps on add(); they merge into Toast.Action.

React
'use client' import { useEffect, useMemo } from 'react' import Toast, { createToastManager } from '@radui/ui/Toast' import type { ToastData } from '@radui/ui/Toast' import Button from '@radui/ui/Button' function Toaster() { const { toasts } = Toast.useToastManager() return ( <Toast.Portal> <Toast.Viewport> {toasts.map((t: ToastData) => ( <Toast.Root key={t.id} toast={t}> <Toast.Content> <Toast.Title>{t.title}</Toast.Title> {t.description ? ( <Toast.Description>{t.description}</Toast.Description> ) : null} {t.actionProps != null ? <Toast.Action /> : null} <Toast.Close /> </Toast.Content> </Toast.Root> ))} </Toast.Viewport> </Toast.Portal> ) } function ActionInner() { const manager = Toast.useToastManager() useEffect(() => () => { manager.dismissAll() }, [manager]) return ( <div className="flex w-full max-w-xl flex-col gap-3"> <Toaster /> <p className="text-sm text-gray-800"> Set <code className="text-xs">actionProps</code> on <code className="text-xs">add()</code>; they merge into <code className="text-xs">Toast.Action</code>. </p> <Button type="button" onClick={() => { manager.add({ title: 'Unread messages', description: 'You have new mail in your inbox.', actionProps: { children: 'Open', onClick: () => manager.close(), }, }) }}> Toast with action </Button> </div> ) } export default function ToastActionExample() { // Each example owns its queue; the default singleton would show every toast in every example on the page. const toastManager = useMemo(() => createToastManager(), []) return ( <Toast.Provider toastManager={toastManager} position="bottom-right" timeout={8000}> <ActionInner /> </Toast.Provider> ) }
preview

limit caps the visible stack. Extra toasts queue until a slot opens.

React
'use client' import { useEffect, useMemo } from 'react' import Toast, { createToastManager } from '@radui/ui/Toast' import type { ToastData } from '@radui/ui/Toast' import Button from '@radui/ui/Button' function Toaster() { const { toasts } = Toast.useToastManager() return ( <Toast.Portal> <Toast.Viewport> {toasts.map((t: ToastData) => ( <Toast.Root key={t.id} toast={t}> <Toast.Content> <Toast.Title>{t.title}</Toast.Title> <Toast.Close /> </Toast.Content> </Toast.Root> ))} </Toast.Viewport> </Toast.Portal> ) } function LimitInner() { const manager = Toast.useToastManager() useEffect(() => () => { manager.dismissAll() }, [manager]) return ( <div className="flex w-full max-w-xl flex-col gap-3"> <Toaster /> <p className="text-sm text-gray-800"> <code className="text-xs">limit</code> caps the visible stack. Extra toasts queue until a slot opens. </p> <div className="flex flex-wrap gap-2"> <Button type="button" onClick={() => manager.add({ title: `Toast ${Date.now()}` })}> Add toast </Button> <Button type="button" onClick={() => manager.close()}> Close all </Button> </div> </div> ) } export default function ToastLimitExample() { // Each example owns its queue; the default singleton would show every toast in every example on the page. const toastManager = useMemo(() => createToastManager(), []) return ( <Toast.Provider toastManager={toastManager} position="bottom-right" gap={14} limit={2}> <LimitInner /> </Toast.Provider> ) }
preview

expand keeps every toast fully visible β€” no stacked peek layout.

React
'use client' import { useEffect, useMemo } from 'react' import Toast, { createToastManager } from '@radui/ui/Toast' import type { ToastData } from '@radui/ui/Toast' import Button from '@radui/ui/Button' function Toaster() { const { toasts } = Toast.useToastManager() return ( <Toast.Portal> <Toast.Viewport> {toasts.map((t: ToastData) => ( <Toast.Root key={t.id} toast={t}> <Toast.Content> <Toast.Title>{t.title}</Toast.Title> <Toast.Close /> </Toast.Content> </Toast.Root> ))} </Toast.Viewport> </Toast.Portal> ) } function ExpandInner() { const manager = Toast.useToastManager() useEffect(() => () => { manager.dismissAll() }, [manager]) return ( <div className="flex w-full max-w-xl flex-col gap-3"> <Toaster /> <p className="text-sm text-gray-800"> <code className="text-xs">expand</code> keeps every toast fully visible β€” no stacked peek layout. </p> <Button type="button" onClick={() => manager.add({ title: `Toast ${Date.now()}` })}> Add toast </Button> </div> ) } export default function ToastExpandExample() { // Each example owns its queue; the default singleton would show every toast in every example on the page. const toastManager = useMemo(() => createToastManager(), []) return ( <Toast.Provider toastManager={toastManager} position="bottom-right" expand maxToasts={5}> <ExpandInner /> </Toast.Provider> ) }