All components
Dialog
Modal windows with backdrop, positioner, and content slots in six widths.
123456789101112131415161718192021222324252627282930import { Dialog } from "#components/ui/base-ui/dialog.tsx";import { Button } from "#components/ui/base-ui/button.tsx";
export function Example() { return ( // size/placement/scrollBehavior resolve once on Root; every part // inherits them via context. Popup renders the centering positioner // around Base UI's popup inside the portal. <Dialog.Root size="md" placement="top"> <Dialog.Trigger render={<Button variant="surface" />}> Open dialog </Dialog.Trigger> <Dialog.Portal> <Dialog.Backdrop /> <Dialog.Popup> <Dialog.Header> <Dialog.Title>Delete project</Dialog.Title> </Dialog.Header> <Dialog.Body> <Dialog.Description>This action cannot be undone.</Dialog.Description> </Dialog.Body> <Dialog.Footer> <Button variant="solid">Delete</Button> </Dialog.Footer> <Dialog.Close aria-label="Close">×</Dialog.Close> </Dialog.Popup> </Dialog.Portal> </Dialog.Root> );}Sizes
xs
sm
md
lg
xl
Install
1npx stylexblocks add dialogSource
components/ui/base-ui/dialog.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409import * as stylex from "@stylexjs/stylex";import { useContext } from "react";import { Dialog as BaseDialog } from "@base-ui-components/react/dialog";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, fromMap, resolveSlotStyles, type StyleOverrideProps,} from "#lib/recipe-props.ts";import { semanticShadows } from "@stylexblocks/ui/semantic-tokens/shadows.stylex.ts";import { durations } from '@stylexblocks/ui/tokens/durations.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 { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'import { zIndices } from '@stylexblocks/ui/tokens/z-indices.stylex.ts'
const dialogBackdrop = stylex.create({ base: { position: 'fixed', left: 0, top: 0, width: '100vw', height: '100dvh', zIndex: zIndices.layer4, animationName: { default: null, ':is([data-open], [data-state="open"])': 'fade-in', ':is([data-closed], [data-state="closed"])': 'fade-out', }, animationDuration: { default: null, ':is([data-open], [data-state="open"])': durations.moderate, ':is([data-closed], [data-state="closed"])': durations.faster, }, },})
const dialogPositioner = stylex.create({ base: { display: 'flex', width: '100vw', height: '100dvh', position: 'fixed', left: 0, top: 0, '--dialog-z-index': zIndices.layer4, zIndex: 'calc(var(--dialog-z-index) + var(--layer-index, 0))', justifyContent: 'center', },})
const dialogPositionerSizes = stylex.create({ cover: { padding: spacing._10, },})
const dialogPositionerPlacements = stylex.create({ center: { alignItems: 'center', }, top: { alignItems: 'flex-start', }, bottom: { alignItems: 'flex-end', },})
const dialogPositionerScrollBehaviors = stylex.create({ inside: { overflow: 'hidden', }, outside: { overflow: 'auto', pointerEvents: 'auto', },})
const dialogContent = stylex.create({ base: { display: 'flex', flexDirection: 'column', position: 'relative', width: '100%', outline: 'none', fontSize: fontSizes.sm, lineHeight: lineHeights.sm, marginBlock: 'var(--dialog-margin, var(--dialog-base-margin))', zIndex: 'calc(var(--dialog-z-index) + var(--layer-index, 0))', '--dialog-padding': spacing._6, '--dialog-radius': semanticRadii.panel, '--dialog-concentric-radius': 'max(0px, calc(var(--dialog-radius) - var(--dialog-padding)))', animationDuration: { default: null, ':is([data-open], [data-state="open"])': durations.moderate, ':is([data-closed], [data-state="closed"])': durations.faster, }, },})
const dialogContentSizes = stylex.create({ xs: { maxWidth: sizes.sm, }, sm: { maxWidth: sizes.md, }, md: { maxWidth: sizes.lg, }, lg: { maxWidth: '2xl', }, xl: { maxWidth: '4xl', }, cover: { width: '100%', height: '100%', '--dialog-margin': '0', }, full: { maxWidth: '100vw', minHeight: '100vh', '--dialog-margin': '0', '--dialog-radius': '0px', borderRadius: 0, },})
const dialogContentVariants = stylex.create({ confirm: { textAlign: 'center', },})
const dialogContentPlacements = stylex.create({ center: { '--dialog-base-margin': 'auto', marginInline: 'auto', }, top: { '--dialog-base-margin': spacing._16, marginInline: 'auto', }, bottom: { '--dialog-base-margin': spacing._16, marginInline: 'auto', },})
const dialogContentScrollBehaviors = stylex.create({ inside: { maxHeight: 'calc(100% - 7.5rem)', },})
const dialogContentMotionPresets = stylex.create({ scale: { animationName: { default: null, ':is([data-open], [data-state="open"])': 'scale-in, fade-in', ':is([data-closed], [data-state="closed"])': 'scale-out, fade-out', }, }, 'slide-in-bottom': { animationName: { default: null, ':is([data-open], [data-state="open"])': 'slide-from-bottom, fade-in', ':is([data-closed], [data-state="closed"])': 'slide-to-bottom, fade-out', }, }, 'slide-in-top': { animationName: { default: null, ':is([data-open], [data-state="open"])': 'slide-from-top, fade-in', ':is([data-closed], [data-state="closed"])': 'slide-to-top, fade-out', }, }, 'slide-in-left': { animationName: { default: null, ':is([data-open], [data-state="open"])': 'slide-from-left, fade-in', ':is([data-closed], [data-state="closed"])': 'slide-to-left, fade-out', }, }, 'slide-in-right': { animationName: { default: null, ':is([data-open], [data-state="open"])': 'slide-from-right, fade-in', ':is([data-closed], [data-state="closed"])': 'slide-to-right, fade-out', }, },})
const dialogHeader = stylex.create({ base: { display: 'flex', flex: 0, paddingInline: spacing._6, paddingBlock: spacing._4, paddingBottom: spacing._2, },})
const dialogHeaderVariants = stylex.create({ confirm: { flexDirection: 'column', alignItems: 'center', },})
const dialogBody = stylex.create({ base: { flex: 1, paddingInline: spacing._6, paddingBlock: spacing._4, },})
const dialogBodyScrollBehaviors = stylex.create({ inside: { overflow: 'auto', },})
const dialogFooter = stylex.create({ base: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: spacing._3, paddingInline: spacing._6, paddingTop: spacing._2, paddingBottom: spacing._4, },})
const dialogFooterVariants = stylex.create({ confirm: { display: 'flex', flexDirection: 'column-reverse', justifyContent: 'stretch', alignItems: 'stretch', },})
const dialogTitle = stylex.create({ base: { fontSize: fontSizes.lg, lineHeight: lineHeights.lg, fontWeight: fontWeights.medium, },})
const dialogDescription = stylex.create({ base: { color: semanticColors.fgMuted, },})
const dialogCloseTrigger = stylex.create({ base: { position: 'absolute', top: spacing._4, right: spacing._4, },})
export type DialogVariant = keyof typeof dialogContentVariants
export type DialogPlacement = keyof typeof dialogPositionerPlacements
export type DialogScrollBehavior = keyof typeof dialogPositionerScrollBehaviors
export type DialogSize = keyof typeof dialogPositionerSizes
export type DialogMotionPreset = keyof typeof dialogContentMotionPresets
// defineSlotRecipe validates the shape at compile time: default values must// exist in the axis maps, and slot keys must be `styles` or a declared axis.// Axis maps are looked up by pluralized axis name (`placement` → `placements`).const dialogRecipe = defineSlotRecipe({ slots: { backdrop: { styles: dialogBackdrop }, positioner: { styles: dialogPositioner, sizes: dialogPositionerSizes, placements: dialogPositionerPlacements, scrollBehaviors: dialogPositionerScrollBehaviors, }, content: { styles: dialogContent, sizes: dialogContentSizes, placements: dialogContentPlacements, scrollBehaviors: dialogContentScrollBehaviors, motionPresets: dialogContentMotionPresets, }, header: { styles: dialogHeader }, body: { styles: dialogBody, scrollBehaviors: dialogBodyScrollBehaviors }, footer: { styles: dialogFooter }, title: { styles: dialogTitle }, description: { styles: dialogDescription }, closeTrigger: { styles: dialogCloseTrigger }, }, defaultVariants: { size: "md", placement: "top", scrollBehavior: "outside", motionPreset: "scale", },});
// Local chrome the recipe leaves to the composition layer: the backdrop's// fixed fill + wash and the panel's surface (plus real max-widths for the// `lg`/`xl` steps, whose generated values are Chakra size names).const chrome = stylex.create({ backdrop: { position: "fixed", inset: 0, backgroundColor: semanticColors.bgBackdrop, }, content: { backgroundColor: semanticColors.bgPanel, borderRadius: "var(--dialog-radius)", boxShadow: semanticShadows.overlay, },});
const contentSizeFix = stylex.create({ lg: { maxWidth: "42rem" }, xl: { maxWidth: "56rem" },});
const { withRootProvider, withContext, VariantContext } = createSlotRecipeContext(dialogRecipe);
const PopupElement = BaseDialog.Popup as React.ElementType;
/** * Base UI has no positioner part, so Popup renders the recipe's centering * positioner around the engine popup — clicking the positioner counts as an * outside press and dismisses. */function Popup( allProps: React.ComponentProps<typeof BaseDialog.Popup> & StyleOverrideProps,) { const { palette, style, className, ...rest } = allProps as Record<string, unknown>; const v = useContext(VariantContext); const positioner = stylex.props( ...(resolveSlotStyles(dialogRecipe.slots.positioner, v) as ReadonlyArray<never>), ); const sx = stylex.props( ...([ palette, ...resolveSlotStyles(dialogRecipe.slots.content, v), chrome.content, fromMap(contentSizeFix, v.size), style, ] as unknown as ReadonlyArray<never>), ); return ( <div {...positioner}> <PopupElement {...rest} className={ typeof className === "string" && className.length > 0 ? [sx.className, className].filter(Boolean).join(" ") : sx.className } style={sx.style} /> </div> );}Popup.displayName = "DialogPopup";
/** * Dialog — Base UI's dialog machine wearing the dialog slot recipe: real * portal to `document.body`, focus trap, scroll lock, and dismiss behavior * all come from the machine. `<Dialog.Root size placement scrollBehavior>` * resolves the recipe once; every part inherits the variants through context * and composes via `render`. */export const Dialog = { Root: withRootProvider(BaseDialog.Root, "DialogRoot"), Trigger: BaseDialog.Trigger, Portal: BaseDialog.Portal, Backdrop: withContext( BaseDialog.Backdrop, (v) => [...resolveSlotStyles(dialogRecipe.slots.backdrop, v), chrome.backdrop], "DialogBackdrop", ), Popup, Header: withContext("div", "header", "DialogHeader"), Body: withContext("div", "body", "DialogBody"), Footer: withContext("div", "footer", "DialogFooter"), Title: withContext(BaseDialog.Title, "title", "DialogTitle"), Description: withContext(BaseDialog.Description, "description", "DialogDescription"), Close: withContext(BaseDialog.Close, "closeTrigger", "DialogClose"),};