Toast
Non-intrusive feedback. Auto-dismiss alerts. Confirm success instantly.
1'use client';23import { Button } from '@/components/ui/button';4import { Toast, useToast } from '@/components/ui/toast';56function ToastDemo() {7 const { toast } = useToast();89 return (10 <Button11 variant="outline"12 onClick={() =>13 toast({14 title: 'Invitation sent',15 description: 'Lucia Mendez gets access to Acme Studio once she accepts.',16 })17 }18 >19 Invite teammate20 </Button>21 );22}2324export function Default() {25 return (26 <Toast.Provider>27 <ToastDemo />28 </Toast.Provider>29 );30}
Installation
Copy and paste the following code into your project.
1'use client';23import { cva, type VariantProps } from 'class-variance-authority';4import { AnimatePresence, motion, useReducedMotion } from 'motion/react';5import * as React from 'react';6import { createPortal } from 'react-dom';7import { cn } from '../lib/cn';8import { springs } from '../lib/motion';9import { useIsClient } from '../lib/overlay';1011type IconProps = React.SVGProps<SVGSVGElement> & {12 size?: number | string;13};1415function AlertCircleIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {16 return (17 <svg18 xmlns="http://www.w3.org/2000/svg"19 width={size}20 height={size}21 viewBox="0 0 24 24"22 fill="none"23 stroke="currentColor"24 strokeWidth={strokeWidth}25 strokeLinecap="round"26 strokeLinejoin="round"27 aria-hidden="true"28 {...props}29 >30 <circle cx="12" cy="12" r="9.5" />31 <path d="M12 7.5V13" />32 <path d="M12 16.5h.01" />33 </svg>34 );35}3637function AlertTriangleIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {38 return (39 <svg40 xmlns="http://www.w3.org/2000/svg"41 width={size}42 height={size}43 viewBox="0 0 24 24"44 fill="none"45 stroke="currentColor"46 strokeWidth={strokeWidth}47 strokeLinecap="round"48 strokeLinejoin="round"49 aria-hidden="true"50 {...props}51 >52 <path d="M10.3 4.2 2.9 17a2 2 0 0 0 1.7 3h14.8a2 2 0 0 0 1.7-3L13.7 4.2a2 2 0 0 0-3.4 0Z" />53 <path d="M12 9v4.5" />54 <path d="M12 16.5h.01" />55 </svg>56 );57}5859function CheckCircleIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {60 return (61 <svg62 xmlns="http://www.w3.org/2000/svg"63 width={size}64 height={size}65 viewBox="0 0 24 24"66 fill="none"67 stroke="currentColor"68 strokeWidth={strokeWidth}69 strokeLinecap="round"70 strokeLinejoin="round"71 aria-hidden="true"72 {...props}73 >74 <circle cx="12" cy="12" r="9.5" />75 <path d="m8 12 2.7 2.7L16 9.3" />76 </svg>77 );78}7980function InfoIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {81 return (82 <svg83 xmlns="http://www.w3.org/2000/svg"84 width={size}85 height={size}86 viewBox="0 0 24 24"87 fill="none"88 stroke="currentColor"89 strokeWidth={strokeWidth}90 strokeLinecap="round"91 strokeLinejoin="round"92 aria-hidden="true"93 {...props}94 >95 <circle cx="12" cy="12" r="9.5" />96 <path d="M12 11v5.5" />97 <path d="M12 7.5h.01" />98 </svg>99 );100}101102function XIcon({ size = 24, strokeWidth = 1.5, ...props }: IconProps) {103 return (104 <svg105 xmlns="http://www.w3.org/2000/svg"106 width={size}107 height={size}108 viewBox="0 0 24 24"109 fill="none"110 stroke="currentColor"111 strokeWidth={strokeWidth}112 strokeLinecap="round"113 strokeLinejoin="round"114 aria-hidden="true"115 {...props}116 >117 <path d="M18 6 6 18" />118 <path d="m6 6 12 12" />119 </svg>120 );121}122123type ToastPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';124125const POSITION_CLASSES: Record<ToastPosition, string> = {126 'top-left':127 'top-0 left-0 flex-col pt-[max(1rem,env(safe-area-inset-top))] pl-[max(1rem,env(safe-area-inset-left))]',128 'top-right':129 'top-0 right-0 flex-col items-end pt-[max(1rem,env(safe-area-inset-top))] pr-[max(1rem,env(safe-area-inset-right))]',130 'bottom-left':131 'bottom-0 left-0 flex-col pb-[max(1rem,env(safe-area-inset-bottom))] pl-[max(1rem,env(safe-area-inset-left))]',132 'bottom-right':133 'bottom-0 right-0 flex-col items-end pb-[max(1rem,env(safe-area-inset-bottom))] pr-[max(1rem,env(safe-area-inset-right))]',134} as const;135136const TOAST_ENTER = {137 initial: { opacity: 0, y: 20, scale: 0.95 },138 animate: { opacity: 1, y: 0, scale: 1 },139} as const;140141const TOAST_EXIT_BY_POSITION: Record<ToastPosition, Record<string, number | string>> = {142 'top-left': { opacity: 0, x: -100, scale: 0.95 },143 'top-right': { opacity: 0, x: 100, scale: 0.95 },144 'bottom-left': { opacity: 0, x: -100, scale: 0.95 },145 'bottom-right': { opacity: 0, x: 100, scale: 0.95 },146} as const;147148const TOAST_TRANSITION = springs.smooth;149150const TOAST_EXIT_TRANSITION = { duration: 0.2, ease: 'easeIn' } as const;151const REDUCED_MOTION_PROPS = {152 initial: { opacity: 0 },153 animate: { opacity: 1 },154 exit: { opacity: 0 },155 transition: { duration: 0.12 },156} as const;157158const DEFAULT_DURATION = 5000;159const DEFAULT_ACTION_DURATION = 10000;160const SWIPE_DISTANCE = 80;161const SWIPE_VELOCITY = 500;162163const toastVariants = cva(164 'pointer-events-auto relative flex w-full items-center gap-3 overflow-hidden rounded-md border p-3.5 bg-popover shadow-md touch-pan-y focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',165 {166 variants: {167 variant: {168 default: 'text-foreground border-border',169 success: 'text-success-text border-success-border',170 error: 'text-destructive-text border-destructive-border',171 info: 'text-info-text border-info-border',172 warning: 'text-warning-text border-warning-border',173 },174 },175 defaultVariants: {176 variant: 'default',177 },178 },179);180181type ToastVariant = VariantProps<typeof toastVariants>['variant'];182183const VARIANT_ICONS: Record<string, React.ComponentType<IconProps> | undefined> = {184 default: undefined,185 success: CheckCircleIcon,186 error: AlertCircleIcon,187 info: InfoIcon,188 warning: AlertTriangleIcon,189};190191interface ToastData {192 id: string;193 title: string;194 description?: string;195 variant?: ToastVariant;196 duration?: number;197 action?: {198 label: string;199 onClick: () => void;200 };201}202203type ToastOptions = Omit<ToastData, 'id'>;204205interface ToastContextType {206 toast: (options: ToastOptions) => string;207 dismiss: (id: string) => void;208}209210const ToastContext = React.createContext<ToastContextType | undefined>(undefined);211212const useToast = () => {213 const context = React.use(ToastContext);214 if (!context) {215 throw new Error('useToast must be used within a Toast.Provider');216 }217 return context;218};219220function isUrgent(toast: ToastData) {221 return toast.variant === 'error' || toast.variant === 'warning';222}223224interface ToastItemProps {225 toast: ToastData;226 onDismiss: (id: string) => void;227 position: ToastPosition;228 dismissLabel: string;229 regionPaused: boolean;230}231232function ToastItem({ toast: t, onDismiss, position, dismissLabel, regionPaused }: ToastItemProps) {233 const [hovered, setHovered] = React.useState(false);234 const [pressed, setPressed] = React.useState(false);235 const [dragging, setDragging] = React.useState(false);236 const paused = hovered || pressed || dragging || regionPaused;237 const shouldReduceMotion = useReducedMotion();238 const duration = t.duration ?? (t.action ? DEFAULT_ACTION_DURATION : DEFAULT_DURATION);239 const persistent = !Number.isFinite(duration) || duration <= 0;240 const remainingRef = React.useRef(duration);241 const titleId = React.useId();242243 React.useEffect(() => {244 remainingRef.current = duration;245 }, [duration]);246247 React.useEffect(() => {248 if (persistent || paused) return;249250 const startedAt = Date.now();251 const timer = setTimeout(() => {252 onDismiss(t.id);253 }, remainingRef.current);254255 return () => {256 clearTimeout(timer);257 const elapsed = Date.now() - startedAt;258 remainingRef.current = Math.max(remainingRef.current - elapsed, 0);259 };260 }, [t.id, persistent, onDismiss, paused]);261262 const VariantIcon = VARIANT_ICONS[t.variant ?? 'default'];263264 return (265 <motion.div266 layout267 initial={shouldReduceMotion ? REDUCED_MOTION_PROPS.initial : TOAST_ENTER.initial}268 animate={shouldReduceMotion ? REDUCED_MOTION_PROPS.animate : TOAST_ENTER.animate}269 exit={270 shouldReduceMotion271 ? REDUCED_MOTION_PROPS.exit272 : { ...TOAST_EXIT_BY_POSITION[position], transition: TOAST_EXIT_TRANSITION }273 }274 transition={shouldReduceMotion ? REDUCED_MOTION_PROPS.transition : TOAST_TRANSITION}275 drag="x"276 dragSnapToOrigin277 dragElastic={0.6}278 onDragStart={() => setDragging(true)}279 onDragEnd={(_, info) => {280 setDragging(false);281 if (282 Math.abs(info.offset.x) > SWIPE_DISTANCE ||283 Math.abs(info.velocity.x) > SWIPE_VELOCITY284 ) {285 onDismiss(t.id);286 }287 }}288 role="group"289 aria-labelledby={titleId}290 tabIndex={-1}291 data-toast-id={t.id}292 onPointerEnter={(event) => {293 if (event.pointerType === 'mouse') setHovered(true);294 }}295 onPointerLeave={(event) => {296 if (event.pointerType === 'mouse') setHovered(false);297 else setPressed(false);298 }}299 onPointerDown={(event) => {300 if (event.pointerType !== 'mouse') setPressed(true);301 }}302 onPointerUp={() => setPressed(false)}303 onPointerCancel={() => setPressed(false)}304 className={cn(toastVariants({ variant: t.variant }), 'flex-col')}305 >306 <div className="flex w-full items-center gap-2">307 {VariantIcon && <VariantIcon className="shrink-0" size={18} />}308 <p id={titleId} className="flex-1 text-sm font-semibold">309 {t.title}310 </p>311 <button312 type="button"313 aria-label={dismissLabel}314 onClick={() => onDismiss(t.id)}315 className="text-muted-foreground hover:text-foreground focus-visible:ring-ring -my-1 -me-1.5 inline-flex size-7 shrink-0 items-center justify-center rounded-sm transition-colors focus-visible:ring-2 focus-visible:outline-none pointer-coarse:-my-2.5 pointer-coarse:-me-3 pointer-coarse:size-10"316 >317 <XIcon size={16} aria-hidden="true" />318 </button>319 </div>320 {(t.description || t.action) && (321 <>322 <div className="text-muted-foreground w-full">323 {t.description && <p className="text-sm">{t.description}</p>}324 {t.action && (325 <button326 type="button"327 onClick={() => {328 t.action?.onClick();329 onDismiss(t.id);330 }}331 className="hover:bg-muted focus-visible:ring-ring mt-2 shrink-0 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none"332 >333 {t.action.label}334 </button>335 )}336 </div>337 </>338 )}339 </motion.div>340 );341}342343function ToastAnnouncer({ toasts, urgent }: { toasts: ToastData[]; urgent: boolean }) {344 return (345 <div className="sr-only" aria-live={urgent ? 'assertive' : 'polite'} aria-atomic="false">346 {toasts347 .filter((t) => isUrgent(t) === urgent)348 .map((t) => (349 <p key={t.id}>350 {t.title}351 {t.description ? ` ${t.description}` : null}352 </p>353 ))}354 </div>355 );356}357358const DEFAULT_MAX_TOASTS = 5;359const DEFAULT_HOTKEY = ['F8'];360361const MODIFIER_KEYS = {362 altKey: 'Alt',363 ctrlKey: 'Control',364 metaKey: 'Meta',365 shiftKey: 'Shift',366} as const;367368function isModifier(key: string): key is keyof typeof MODIFIER_KEYS {369 return key in MODIFIER_KEYS;370}371372function matchesHotkey(event: KeyboardEvent, hotkey: readonly string[]) {373 if (hotkey.length === 0) return false;374 return hotkey.every((key) =>375 isModifier(key) ? event[key] : event.code === key || event.key === key,376 );377}378379function describeHotkey(hotkey: readonly string[]) {380 return hotkey381 .map((key) => (isModifier(key) ? MODIFIER_KEYS[key] : key.replace(/^(Key|Digit)/, '')))382 .join('+');383}384385interface ToastLabels {386 region: string;387 dismiss: string;388}389390const DEFAULT_LABELS: ToastLabels = {391 region: 'Notifications',392 dismiss: 'Dismiss notification',393};394395interface ToastProviderProps {396 children: React.ReactNode;397 maxToasts?: number;398 position?: ToastPosition;399 labels?: Partial<ToastLabels>;400 hotkey?: string[];401}402403function ToastProvider({404 children,405 maxToasts = DEFAULT_MAX_TOASTS,406 position = 'bottom-right',407 labels: labelOverrides,408 hotkey = DEFAULT_HOTKEY,409 ref,410}: ToastProviderProps & { ref?: React.Ref<HTMLDivElement> }) {411 const [toasts, setToasts] = React.useState<ToastData[]>([]);412 const [regionFocused, setRegionFocused] = React.useState(false);413 const mounted = useIsClient();414 const labels = { ...DEFAULT_LABELS, ...labelOverrides };415 const regionRef = React.useRef<HTMLDivElement | null>(null);416 const toastsRef = React.useRef<ToastData[]>(toasts);417 const returnFocusRef = React.useRef<HTMLElement | null>(null);418 const hotkeyId = hotkey.join('+');419420 const findToastElement = React.useCallback((id: string | undefined) => {421 if (id === undefined) return null;422 const items = regionRef.current?.querySelectorAll<HTMLElement>('[data-toast-id]') ?? [];423 return Array.from(items).find((item) => item.dataset.toastId === id) ?? null;424 }, []);425426 React.useLayoutEffect(() => {427 toastsRef.current = toasts;428 const active = document.activeElement;429 if (!(active instanceof HTMLElement)) return;430 const host = active.closest<HTMLElement>('[data-toast-id]');431 if (!host || !regionRef.current?.contains(host)) return;432 if (toasts.some((t) => t.id === host.dataset.toastId)) return;433 const newest = findToastElement(toasts[toasts.length - 1]?.id);434 const returnTo = returnFocusRef.current;435 if (newest) newest.focus();436 else if (returnTo?.isConnected) returnTo.focus();437 else active.blur();438 }, [toasts, findToastElement]);439440 const dismiss = React.useCallback(441 (id: string) => {442 const current = toastsRef.current;443 const index = current.findIndex((t) => t.id === id);444 const element = findToastElement(id);445 const active = document.activeElement;446447 if (index !== -1 && element && active && element.contains(active)) {448 const neighbor = findToastElement(current[index + 1]?.id ?? current[index - 1]?.id);449 const returnTo = returnFocusRef.current;450 if (neighbor) neighbor.focus();451 else if (returnTo?.isConnected) returnTo.focus();452 else if (active instanceof HTMLElement) active.blur();453 }454455 setToasts((prev) => prev.filter((t) => t.id !== id));456 },457 [findToastElement],458 );459460 const toast = React.useCallback(461 (options: ToastOptions): string => {462 const id =463 typeof crypto !== 'undefined' && crypto.randomUUID464 ? crypto.randomUUID()465 : `toast-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;466 const newToast: ToastData = { ...options, id };467468 setToasts((prev) => {469 const next = [...prev, newToast];470 if (next.length > maxToasts) {471 return next.slice(-maxToasts);472 }473 return next;474 });475476 return id;477 },478 [maxToasts],479 );480481 React.useEffect(() => {482 const keys = hotkeyId ? hotkeyId.split('+') : [];483 const handleKeyDown = (event: KeyboardEvent) => {484 if (!matchesHotkey(event, keys)) return;485 const newest = findToastElement(toastsRef.current[toastsRef.current.length - 1]?.id);486 if (!newest) return;487 event.preventDefault();488 const active = document.activeElement;489 if (active instanceof HTMLElement && !regionRef.current?.contains(active)) {490 returnFocusRef.current = active;491 }492 newest.focus();493 };494 document.addEventListener('keydown', handleKeyDown);495 return () => document.removeEventListener('keydown', handleKeyDown);496 }, [hotkeyId, findToastElement]);497498 const setRegionRef = React.useCallback(499 (node: HTMLDivElement | null) => {500 regionRef.current = node;501 if (typeof ref === 'function') ref(node);502 else if (ref) ref.current = node;503 },504 [ref],505 );506507 const contextValue = React.useMemo(() => ({ toast, dismiss }), [toast, dismiss]);508509 return (510 <ToastContext value={contextValue}>511 {children}512 <ToastAnnouncer toasts={toasts} urgent={false} />513 <ToastAnnouncer toasts={toasts} urgent />514 {mounted &&515 createPortal(516 <div517 role="region"518 ref={setRegionRef}519 aria-label={labels.region}520 aria-keyshortcuts={hotkey.length > 0 ? describeHotkey(hotkey) : undefined}521 onFocus={(event) => {522 const from = event.relatedTarget;523 if (from instanceof HTMLElement && !event.currentTarget.contains(from)) {524 returnFocusRef.current = from;525 }526 setRegionFocused(true);527 }}528 onBlur={(event) => {529 if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {530 setRegionFocused(false);531 }532 }}533 className={cn(534 'pointer-events-none fixed z-9999 flex max-h-dvh w-full max-w-sm gap-2 p-4',535 POSITION_CLASSES[position],536 )}537 >538 <AnimatePresence mode="popLayout">539 {toasts.map((t) => (540 <ToastItem541 key={t.id}542 toast={t}543 onDismiss={dismiss}544 position={position}545 dismissLabel={labels.dismiss}546 regionPaused={regionFocused}547 />548 ))}549 </AnimatePresence>550 </div>,551 document.body,552 )}553 </ToastContext>554 );555}556557ToastProvider.displayName = 'ToastProvider';558559const Toast = {560 Provider: ToastProvider,561};562563export { Toast, toastVariants, useToast };564export type {565 ToastData,566 ToastLabels,567 ToastOptions,568 ToastPosition,569 ToastProviderProps,570 ToastVariant,571};
Update the import paths to match your project structure.
Anatomy
1import { Toast, useToast } from '@/components/ui/toast';
Wrap your app with
Toast.Provider, then use useToast() anywhere:1function App() {2 return (3 <Toast.Provider>4 <MyComponent />5 </Toast.Provider>6 );7}89function MyComponent() {10 const { toast } = useToast();1112 return <button onClick={() => toast({ title: 'Hello!' })}>Show Toast</button>;13}
Variants
Default
1'use client';23import { Button } from '@/components/ui/button';4import { Toast, useToast } from '@/components/ui/toast';56function ToastDemo() {7 const { toast } = useToast();89 return (10 <Button11 variant="outline"12 onClick={() =>13 toast({14 title: 'Invitation sent',15 description: 'Lucia Mendez gets access to Acme Studio once she accepts.',16 })17 }18 >19 Invite teammate20 </Button>21 );22}2324export function Default() {25 return (26 <Toast.Provider>27 <ToastDemo />28 </Toast.Provider>29 );30}
All Variants
Five built-in variants:
default, success, error, info, and warning.1'use client';23import { Button } from '@/components/ui/button';4import { Toast, useToast } from '@/components/ui/toast';56const variants = [7 {8 label: 'Default',9 options: {10 title: 'Invitation sent',11 description: 'Lucia Mendez gets access to Acme Studio once she accepts.',12 },13 },14 {15 label: 'Success',16 options: {17 title: 'Deployed to production',18 description: 'checkout-flow built in 42s and is serving traffic.',19 variant: 'success' as const,20 },21 },22 {23 label: 'Error',24 options: {25 title: 'Payment declined',26 description: 'Visa ending 4242 was rejected by the issuing bank.',27 variant: 'error' as const,28 },29 },30 {31 label: 'Info',32 options: {33 title: 'Export is being prepared',34 description: 'We will email a download link to lucia@acmestudio.dev when it is ready.',35 variant: 'info' as const,36 },37 },38 {39 label: 'Warning',40 options: {41 title: 'Approaching the rate limit',42 description: 'You have used 9,400 of 10,000 requests this hour.',43 variant: 'warning' as const,44 },45 },46];4748function VariantsDemo() {49 const { toast } = useToast();5051 return (52 <div className="flex flex-wrap gap-2">53 {variants.map((variant) => (54 <Button key={variant.label} variant="outline" onClick={() => toast(variant.options)}>55 {variant.label}56 </Button>57 ))}58 </div>59 );60}6162export function Variants() {63 return (64 <Toast.Provider>65 <VariantsDemo />66 </Toast.Provider>67 );68}
With Action
Toast with an action button. Toasts with an action stay up for 10 seconds instead of 5, so there is time to reach the button.
checkout-flow
Last deploy Mar 14, 3 environments
Active
1'use client';23import * as React from 'react';4import { Badge } from '@/components/ui/badge';5import { Button } from '@/components/ui/button';6import { Toast, useToast } from '@/components/ui/toast';7import { Flex } from '@/components/ui/flex';89function WithActionDemo() {10 const { toast } = useToast();11 const [archived, setArchived] = React.useState(false);1213 const archive = () => {14 setArchived(true);15 toast({16 title: 'Project archived',17 description: 'checkout-flow stopped serving traffic and moved to the archive.',18 action: {19 label: 'Undo',20 onClick: () => setArchived(false),21 },22 });23 };2425 return (26 <Flex27 align="center"28 justify="between"29 gap="4"30 className="border-border bg-card w-full max-w-md rounded-xl border p-4"31 >32 <div className="min-w-0">33 <p className="text-sm font-medium">checkout-flow</p>34 <p className="text-muted-foreground text-xs">Last deploy Mar 14, 3 environments</p>35 </div>36 <Flex align="center" gap="2">37 <Badge variant={archived ? 'secondary' : 'success'}>38 {archived ? 'Archived' : 'Active'}39 </Badge>40 <Button variant="outline" size="sm" disabled={archived} onClick={archive}>41 Archive42 </Button>43 </Flex>44 </Flex>45 );46}4748export function WithAction() {49 return (50 <Toast.Provider>51 <WithActionDemo />52 </Toast.Provider>53 );54}
Positions
Choose where toasts appear on screen. Click a position to select it, then trigger a toast.
1'use client';23import * as React from 'react';4import { Button } from '@/components/ui/button';5import { Toast, useToast, type ToastPosition } from '@/components/ui/toast';67const positions: { label: string; value: ToastPosition }[] = [8 { label: 'Top Left', value: 'top-left' },9 { label: 'Top Right', value: 'top-right' },10 { label: 'Bottom Left', value: 'bottom-left' },11 { label: 'Bottom Right', value: 'bottom-right' },12];1314function PositionsInner() {15 const { toast } = useToast();1617 return (18 <Button19 variant="outline"20 onClick={() =>21 toast({22 title: 'Backup finished',23 description: 'Nightly snapshot of acme-prod stored in eu-central-1.',24 variant: 'success',25 })26 }27 >28 Run backup now29 </Button>30 );31}3233export function Positions() {34 const [position, setPosition] = React.useState<ToastPosition>('bottom-right');3536 return (37 <Toast.Provider position={position}>38 <div className="space-y-4">39 <div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap">40 {positions.map((pos) => (41 <Button42 key={pos.value}43 variant={position === pos.value ? 'default' : 'outline'}44 size="sm"45 onClick={() => setPosition(pos.value)}46 >47 {pos.label}48 </Button>49 ))}50 </div>51 <PositionsInner />52 </div>53 </Toast.Provider>54 );55}
Accessibility
- Messages are read out through live regions that stay mounted: errors and warnings interrupt, everything else waits its turn.
- Press
F8to jump to the newest toast, thenTabthrough its buttons. Change the shortcut withhotkey. - Timers pause while the pointer is over a toast, while focus is inside the stack and while a finger rests on it.
- Swipe a toast sideways to dismiss it on touch screens.
- When a focused toast closes, focus moves to the next one, or back to where it was before you entered the stack.
API Reference
Toast.Provider
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | โ | The app content. |
maxToasts | number | 5 | Maximum number of visible toasts. |
position | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "bottom-right" | Where toasts appear on screen. |
labels | Partial<ToastLabels> | - | Accessible names: region ("Notifications") and dismiss ("Dismiss notification"). |
hotkey | string[] | ["F8"] | Keys that move focus to the newest toast. Use modifier names ("altKey", "ctrlKey", "metaKey", "shiftKey") plus a key code, like ["altKey", "KeyT"]. Pass [] to turn it off. |
useToast()
Returns an object with:
| Method | Type | Description |
|---|---|---|
toast | (options: ToastOptions) => string | Shows a toast, returns its ID. |
dismiss | (id: string) => void | Dismisses a toast by ID. |
ToastOptions
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | โ | Toast title (required). |
description | string | โ | Optional description text. |
variant | "default" | "success" | "error" | "info" | "warning" | "default" | Visual variant. |
duration | number | 5000 | Auto-dismiss delay in ms. 10000 when there is an action. Set 0 or Infinity to keep it until dismissed. |
action | { label: string; onClick: () => void } | โ | Optional action button. |
Found something to improve?
Notice a bug, typo, or missing detail on this page? Help us make the documentation better by opening a GitHub issue.