All components
Toast
Transient notifications with indicator, title, description, action, and close slots.
1234567891011121314151617181920212223242526272829import { Toast, Toaster } from "#components/ui/base-ui/toast.tsx";
// Base UI's real toast manager: wrap the app in Toast.Provider, mount// <Toaster /> once, and queue toasts with Toast.useToastManager().function SaveButton() { const manager = Toast.useToastManager(); return ( <button onClick={() => manager.add({ title: "Changes saved", description: "Your theme has been published.", type: "success", }) } > Save </button> );}
export function App() { return ( <Toast.Provider> <SaveButton /> <Toaster /> </Toast.Provider> );}Install
1npx stylexblocks add toastSource
components/ui/base-ui/toast.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318import * as stylex from "@stylexjs/stylex";import { Toast as BaseToast } from "@base-ui-components/react/toast";import { mergeProps } from "@base-ui-components/react/merge-props";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, resolveSlotStyles } from "#lib/recipe-props.ts";import { zIndices } from "@stylexblocks/ui/tokens/z-indices.stylex.ts";import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { cursor } from '@stylexblocks/ui/tokens/cursor.stylex.ts'import { easings } from '@stylexblocks/ui/tokens/easings.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { radii } from '@stylexblocks/ui/tokens/radii.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticShadows } from '@stylexblocks/ui/semantic-tokens/shadows.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'
const toastRoot = stylex.create({ base: { width: sizes.full, display: 'flex', alignItems: 'flex-start', position: 'relative', gap: spacing._2, paddingBlock: spacing._3, paddingInlineStart: spacing._3, paddingInlineEnd: spacing._6, borderRadius: radii.md, borderWidth: '1px', translate: 'var(--x) var(--y)', scale: 'var(--scale)', zIndex: 'var(--z-index)', height: 'var(--height, var(--toast-height))', opacity: 'var(--opacity)', willChange: 'translate, opacity, scale, height', transition: 'translate 400ms, scale 400ms, opacity 400ms, height 200ms, box-shadow 200ms', transitionTimingFunction: easings.bounceIn, backgroundColor: semanticColors.bgPanel, color: semanticColors.fg, boxShadow: semanticShadows.lg, overflow: 'hidden', '--toast-indicated-color': semanticColors.fgMuted, },})
const toastTitle = stylex.create({ base: { fontWeight: fontWeights.medium, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, marginInlineEnd: spacing._2, },})
const toastDescription = stylex.create({ base: { display: 'inline', fontSize: fontSizes.sm, lineHeight: lineHeights.sm, opacity: 0.8, },})
const toastIndicator = stylex.create({ base: { flexShrink: 0, width: sizes._5, height: sizes._5, color: 'var(--toast-indicated-color)', },})
const toastActionTrigger = stylex.create({ base: { cursor: cursor.button, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, fontWeight: fontWeights.medium, height: sizes._6, paddingInline: spacing._3, marginInlineStart: `calc(${spacing._3} * -1)`, borderRadius: radii.sm, alignSelf: 'flex-start', transition: 'background 200ms', color: { default: `color-mix(in oklch, ${colorPalette.solid} 80%, transparent)`, ':hover': colorPalette.solid, }, backgroundColor: { default: null, ':hover': semanticColors.bgSubtle, }, },})
const toastCloseTrigger = stylex.create({ base: { position: 'absolute', top: spacing._2, insetInlineEnd: spacing._2, },})
// Local chrome the recipe leaves to the composition layer: logical-property// fixes for generated Chakra shorthands (`ps`/`pe`/`ms`/`marginEnd`/// `insetEnd`), the viewport's fixed stack in the bottom-end corner (above// all page chrome), enter/leave transitions keyed off Base UI's// `data-starting-style` / `data-ending-style`, the close trigger's// icon-button look, and per-type indicator colors.const chrome = stylex.create({ viewport: { position: "fixed", bottom: spacing._4, insetInlineEnd: spacing._4, display: "flex", flexDirection: "column", gap: spacing._2, width: "22rem", maxWidth: "calc(100vw - 2rem)", margin: 0, padding: 0, listStyle: "none", zIndex: zIndices.toast, outline: 0, }, root: { maxWidth: "calc(100vw - 2rem)", paddingInlineStart: spacing._3, paddingInlineEnd: spacing._6, width: "22.5rem", transitionProperty: "opacity, transform", transitionDuration: "250ms", transitionTimingFunction: "cubic-bezier(0.21, 1.02, 0.73, 1)", opacity: { default: 1, ":is([data-starting-style])": 0, ":is([data-ending-style])": 0, }, transform: { default: "translateY(0)", ":is([data-starting-style])": "translateY(0.75rem)", ":is([data-ending-style])": "translateY(0.75rem)", }, }, title: { marginInlineEnd: spacing._2, }, actionTrigger: { marginInlineStart: "-0.75rem", backgroundColor: "transparent", borderStyle: "none", }, closeTrigger: { insetInlineEnd: spacing._2, display: "inline-flex", alignItems: "center", justifyContent: "center", width: "1.5rem", height: "1.5rem", borderRadius: radii.sm, borderStyle: "none", color: semanticColors.fgMuted, backgroundColor: { default: "transparent", ":hover": semanticColors.bgSubtle }, cursor: "pointer", }, body: { flex: 1, minWidth: 0, },});
function typeColorFor(type?: string) { return ( (type === "success" && typeColor.success) || (type === "error" && typeColor.error) || (type === "warning" && typeColor.warning) || (type === "info" && typeColor.info) || undefined );}
const typeColor = stylex.create({ success: { "--toast-indicated-color": semanticColors.fgSuccess }, error: { "--toast-indicated-color": semanticColors.fgError }, warning: { "--toast-indicated-color": semanticColors.fgWarning }, info: { "--toast-indicated-color": semanticColors.fgInfo },});
const toastRecipe = defineSlotRecipe({ slots: { root: { styles: toastRoot }, title: { styles: toastTitle }, description: { styles: toastDescription }, indicator: { styles: toastIndicator }, actionTrigger: { styles: toastActionTrigger }, closeTrigger: { styles: toastCloseTrigger }, }, defaultVariants: {},});
const { withProvider, withContext } = createSlotRecipeContext(toastRecipe);
function ToastRootNode(props: React.ComponentProps<typeof BaseToast.Root>) { const { toast, ...rest } = props; return ( <BaseToast.Root toast={toast} {...mergeProps(rest, stylex.props(typeColorFor(toast.type)))} /> );}
function TypeIcon({ type }: { type?: string }) { if (type === "success") { return ( <svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true"> <circle cx="10" cy="10" r="7.5" stroke="currentColor" strokeWidth="1.5" /> <path d="M7 10.2l2 2 4-4.4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> ); } if (type === "error" || type === "warning") { return ( <svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true"> <circle cx="10" cy="10" r="7.5" stroke="currentColor" strokeWidth="1.5" /> <path d="M10 6.5v4.2M10 13.4v.1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> </svg> ); } return ( <svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true"> <circle cx="10" cy="10" r="7.5" stroke="currentColor" strokeWidth="1.5" /> <path d="M10 9.4v4M10 6.6v.1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> </svg> );}
function CloseIcon() { return ( <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"> <path d="M2.5 2.5l7 7m0-7l-7 7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> </svg> );}
/** * Toast — Base UI's real toast machine: wrap the app in `<Toast.Provider>`, * call `Toast.useToastManager().add({ title, description, type })` from * anywhere, and mount `<Toaster />` once inside the provider — it portals * the viewport to `document.body` and renders every queued toast through the * toast slot recipe with timers, pause on hover/focus, swipe-to-dismiss, and * a11y announcements handled by the engine. */export const Toast = { Provider: BaseToast.Provider, Portal: BaseToast.Portal, Viewport: withContext( BaseToast.Viewport, () => [chrome.viewport], "ToastViewport", ), Root: withProvider( ToastRootNode, () => [...resolveSlotStyles(toastRecipe.slots.root, {}), chrome.root], "ToastRoot", ), Title: withContext( BaseToast.Title, () => [...resolveSlotStyles(toastRecipe.slots.title, {}), chrome.title], "ToastTitle", ), Description: withContext(BaseToast.Description, "description", "ToastDescription"), Indicator: withContext("span", "indicator", "ToastIndicator"), Action: withContext( BaseToast.Action, () => [...resolveSlotStyles(toastRecipe.slots.actionTrigger, {}), chrome.actionTrigger], "ToastAction", ), Close: withContext( BaseToast.Close, () => [...resolveSlotStyles(toastRecipe.slots.closeTrigger, {}), chrome.closeTrigger], "ToastClose", ), useToastManager: BaseToast.useToastManager, createToastManager: BaseToast.createToastManager,};
function ToastList() { const { toasts } = BaseToast.useToastManager(); return toasts.map((toast) => ( <Toast.Root key={toast.id} toast={toast}> <Toast.Indicator> <TypeIcon type={toast.type} /> </Toast.Indicator> <div {...stylex.props(chrome.body)}> <Toast.Title /> <Toast.Description /> </div> <Toast.Close aria-label="Dismiss notification"> <CloseIcon /> </Toast.Close> </Toast.Root> ));}
export function Toaster() { return ( <BaseToast.Portal> <Toast.Viewport> <ToastList /> </Toast.Viewport> </BaseToast.Portal> );}Toaster.displayName = "Toaster";