All components
Toast
Transient notifications with indicator, title, description, action, and close slots.
1234567891011121314151617181920212223import { Toaster, toaster } from "#components/ui/ark-ui/toast.tsx";
// toaster is the store from Ark's createToaster — call it from anywhere;// <Toaster /> is the portal-mounted region rendering each toast through the// toast slot recipe.export function App() { return ( <> <button onClick={() => toaster.create({ title: "Changes saved", description: "Your theme has been published.", type: "success", }) } > Save </button> <Toaster /> </> );}Install
1npx stylexblocks add toastSource
components/ui/ark-ui/toast.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294import * as stylex from "@stylexjs/stylex";import { Toast as ArkToast, Toaster as ArkToaster, createToaster } from "@ark-ui/react/toast";import { Portal } from "@ark-ui/react/portal";import { mergeProps } from "@ark-ui/react/utils";import { createSlotRecipeContext } from "#lib/ark-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`), a real easing for the generated `bounce-in` keyword, the// close trigger's icon-button look, per-type indicator colors, and the// toast region's z-index above all page chrome.const chrome = stylex.create({ region: { zIndex: zIndices.toast, "--z-index": zIndices.toast, }, root: { maxWidth: "calc(100vw - 2rem)", paddingInlineStart: spacing._3, paddingInlineEnd: spacing._6, width: "22.5rem", transitionTimingFunction: "cubic-bezier(0.21, 1.02, 0.73, 1)", }, 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({ type, ...props}: React.ComponentProps<typeof ArkToast.Root> & { type?: string }) { return ( <ArkToast.Root {...mergeProps(props, stylex.props(typeColorFor(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 — Ark's real toast machine: `toaster` is the store created with * `createToaster` (call `toaster.create({ title, description, type })` from * anywhere), and `<Toaster />` is the portal-mounted region that renders * each toast through the toast slot recipe with stacking, timers, pause on * hover, and dismiss handled by the machine. */export const toaster = createToaster({ placement: "bottom-end", overlap: true, gap: 16,});
export const Toast = { Root: withProvider( ToastRootNode, () => [...resolveSlotStyles(toastRecipe.slots.root, {}), chrome.root], "ToastRoot", ), Title: withContext( ArkToast.Title, () => [...resolveSlotStyles(toastRecipe.slots.title, {}), chrome.title], "ToastTitle", ), Description: withContext(ArkToast.Description, "description", "ToastDescription"), Indicator: withContext("span", "indicator", "ToastIndicator"), ActionTrigger: withContext( ArkToast.ActionTrigger, () => [ ...resolveSlotStyles(toastRecipe.slots.actionTrigger, {}), chrome.actionTrigger, ], "ToastActionTrigger", ), CloseTrigger: withContext( ArkToast.CloseTrigger, () => [ ...resolveSlotStyles(toastRecipe.slots.closeTrigger, {}), chrome.closeTrigger, ], "ToastCloseTrigger", ),};
export function Toaster() { return ( <Portal> <ArkToaster toaster={toaster} {...stylex.props(chrome.region)}> {(toast) => ( <Toast.Root key={toast.id} type={toast.type}> <Toast.Indicator> <TypeIcon type={toast.type} /> </Toast.Indicator> <div {...stylex.props(chrome.body)}> <Toast.Title>{toast.title}</Toast.Title> <Toast.Description>{toast.description}</Toast.Description> </div> {toast.action ? ( <Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger> ) : null} <Toast.CloseTrigger aria-label="Dismiss notification"> <CloseIcon /> </Toast.CloseTrigger> </Toast.Root> )} </ArkToaster> </Portal> );}Toaster.displayName = "Toaster";