;
+
+interface _DialogOverlayProps extends _DialogOverlayOmit {
+ className?: string;
+ children: ReactNode;
+}
+
+/**
+ * Props for `DialogOverlay`.
+ *
+ * @tier composed
+ */
+export type DialogOverlayProps = Prettify<_DialogOverlayProps>;
+
+/** Backdrop overlay for the dialog. */
+export function DialogOverlay({ className, children, ...props }: DialogOverlayProps): JSX.Element {
+ return (
+
+ {children}
+
+ );
+}
+
+interface _DialogProps {
+ className?: string;
+ children: ReactNode;
+ /**
+ * Whether to show the close button in the top-right corner.
+ * @default true
+ */
+ showCloseButton?: boolean;
+ /**
+ * Whether clicking outside or pressing Escape dismisses the dialog.
+ * @default true
+ */
+ isDismissable?: RacModalOverlayProps['isDismissable'];
+ /** Whether the dialog is open (controlled). */
+ isOpen?: RacModalOverlayProps['isOpen'];
+ /** Whether the dialog is open by default (uncontrolled). */
+ defaultOpen?: RacModalOverlayProps['defaultOpen'];
+ /** Handler called when the dialog's open state changes. */
+ onOpenChange?: RacModalOverlayProps['onOpenChange'];
+}
+
+/**
+ * Props for `Dialog`.
+ *
+ * @tier composed
+ */
+export type DialogProps = Prettify<_DialogProps>;
+
+/** Modal dialog with overlay, content panel, and optional close button. */
+export function Dialog({
+ className,
+ children,
+ isDismissable = true,
+ showCloseButton = true,
+ ...props
+}: DialogProps): JSX.Element {
+ return (
+
+
+
+ {children}
+ {showCloseButton && }
+
+
+
+ );
+}
+
+function InternalCloseButton(): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+/**
+ * Props for `DialogHeader`.
+ *
+ * @tier composed
+ */
+export type DialogHeaderProps = ComponentProps<'div'>;
+
+/** Header section of the dialog, typically containing `DialogTitle` and `DialogDescription`. */
+export function DialogHeader({ className, ...props }: DialogHeaderProps): JSX.Element {
+ return (
+
+ );
+}
+
+/**
+ * Props for `DialogFooter`.
+ *
+ * @tier composed
+ */
+export type DialogFooterProps = ComponentProps<'div'> & {
+ /**
+ * Whether to show a text close button at the end of the footer.
+ * @default false
+ */
+ showCloseButton?: boolean;
+};
+
+/** Footer section of the dialog, typically containing action buttons. */
+export function DialogFooter({
+ className,
+ children,
+ showCloseButton = false,
+ ...props
+}: DialogFooterProps): JSX.Element {
+ return (
+
+ {children}
+ {showCloseButton && Close}
+
+ );
+}
+
+type _DialogTitleOmit = DistributiveOmit;
+
+interface _DialogTitleProps extends _DialogTitleOmit {
+ className?: string;
+}
+
+/**
+ * Props for `DialogTitle`.
+ *
+ * @tier composed
+ */
+export type DialogTitleProps = Prettify<_DialogTitleProps>;
+
+/** Dialog title rendered as a heading. */
+export function DialogTitle({ className, ...props }: _DialogTitleProps): JSX.Element {
+ return (
+
+ );
+}
+
+/**
+ * Props for `DialogDescription`.
+ *
+ * @tier composed
+ */
+export type DialogDescriptionProps = ComponentProps<'div'>;
+
+/** Descriptive text or content within the dialog body. */
+export function DialogDescription({ className, ...props }: DialogDescriptionProps): JSX.Element {
+ return (
+
+ );
+}
diff --git a/packages/@luke-ui/react/src/recipes/dialog.css.ts b/packages/@luke-ui/react/src/recipes/dialog.css.ts
new file mode 100644
index 00000000..04a42ccd
--- /dev/null
+++ b/packages/@luke-ui/react/src/recipes/dialog.css.ts
@@ -0,0 +1,178 @@
+import { keyframes } from '@vanilla-extract/css';
+import type { RecipeVariants } from '@vanilla-extract/recipes';
+import { recipeInLayer } from '../styles/layered-style.css.js';
+import { vars } from '../theme/contract.css.js';
+
+const overlayIn = keyframes({
+ from: { opacity: 0 },
+ to: { opacity: 1 },
+});
+
+const overlayOut = keyframes({
+ from: { opacity: 1 },
+ to: { opacity: 0 },
+});
+
+const contentIn = keyframes({
+ '0%': { opacity: 0, transform: 'translate(-50%, -50%) scale(0.95)' },
+ '100%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
+});
+
+const contentOut = keyframes({
+ '0%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
+ '100%': { opacity: 0, transform: 'translate(-50%, -50%) scale(0.95)' },
+});
+
+export const dialogOverlay = recipeInLayer('recipes', {
+ base: {
+ '@media': {
+ '(prefers-reduced-motion: reduce)': {
+ selectors: {
+ '&[data-entering]': { animation: 'none' },
+ '&[data-exiting]': { animation: 'none' },
+ },
+ },
+ },
+ backgroundColor: 'rgb(0 0 0 / 0.8)',
+ inset: 0,
+ isolation: 'isolate',
+ position: 'fixed',
+ selectors: {
+ '&[data-entering]': { animation: `${overlayIn} 100ms ease-out` },
+ '&[data-exiting]': { animation: `${overlayOut} 100ms ease-in` },
+ },
+ zIndex: 50,
+ },
+});
+
+export const dialogContent = recipeInLayer('recipes', {
+ base: {
+ '@media': {
+ '(prefers-reduced-motion: reduce)': {
+ selectors: {
+ '&[data-entering]': { animation: 'none' },
+ '&[data-exiting]': { animation: 'none' },
+ },
+ },
+ '(min-width: 640px)': {
+ maxWidth: '28rem',
+ },
+ },
+ '@supports': {
+ '(backdrop-filter: blur(4px))': {
+ backdropFilter: 'blur(4px)',
+ },
+ },
+ backgroundColor: vars.color.surface.floating,
+ borderRadius: vars.radius.overlay,
+ border: `1px solid ${vars.color.border.decorative}`,
+ boxShadow: vars.depth.overlay,
+ color: vars.color.text.primary,
+ display: 'grid',
+ gap: vars.space[600],
+ left: '50%',
+ maxWidth: 'calc(100% - 2rem)',
+ outline: 'none',
+ padding: vars.space[600],
+ position: 'fixed',
+ selectors: {
+ '&[data-entering]': { animation: `${contentIn} 100ms ease-out` },
+ '&[data-exiting]': { animation: `${contentOut} 100ms ease-in` },
+ },
+ top: '50%',
+ transform: 'translate(-50%, -50%)',
+ width: '100%',
+ zIndex: 50,
+ },
+});
+
+export const dialog = recipeInLayer('recipes', {
+ base: {
+ display: 'inherit',
+ gap: 'inherit',
+ outline: 'none',
+ },
+});
+
+export const dialogHeader = recipeInLayer('recipes', {
+ base: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: vars.space[200],
+ },
+});
+
+export const dialogFooter = recipeInLayer('recipes', {
+ base: {
+ '@media': {
+ '(min-width: 640px)': {
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ },
+ },
+ display: 'flex',
+ flexDirection: 'column-reverse',
+ gap: vars.space[200],
+ },
+});
+
+export const dialogTitle = recipeInLayer('recipes', {
+ base: {
+ fontFamily: vars.font.family,
+ fontSize: vars.font[400].fontSize,
+ fontWeight: vars.font.weight.heading,
+ lineHeight: 1,
+ margin: 0,
+ },
+});
+
+export const dialogDescription = recipeInLayer('recipes', {
+ base: {
+ color: vars.color.text.secondary,
+ fontSize: vars.font[200].fontSize,
+ },
+});
+
+const closeButtonReset = {
+ alignItems: 'center',
+ appearance: 'none',
+ background: 'none',
+ border: 'none',
+ color: 'inherit',
+ cursor: 'pointer',
+ display: 'inline-flex',
+ font: 'inherit',
+ justifyContent: 'center',
+ outline: 'none',
+ padding: 0,
+} as const;
+
+export const dialogCloseButton = recipeInLayer('recipes', {
+ base: {
+ ...closeButtonReset,
+ blockSize: vars.controlSize.small,
+ inlineSize: vars.controlSize.small,
+ position: 'absolute',
+ insetBlockStart: vars.space[400],
+ insetInlineEnd: vars.space[400],
+ borderRadius: vars.radius.control,
+ selectors: {
+ '&[data-hovered="true"]': {
+ backgroundColor: vars.color.intent.neutral.surface.subtleHover,
+ },
+ '&[data-focus-visible="true"]': {
+ outline: `2px solid ${vars.color.border.focus}`,
+ outlineOffset: '2px',
+ },
+ },
+ },
+});
+
+export type DialogOverlayVariants = RecipeVariants;
+export type DialogContentVariants = RecipeVariants;
+export type DialogVariants = RecipeVariants;
+export type DialogCloseButtonVariants = RecipeVariants;
+export type DialogHeaderVariants = RecipeVariants;
+export type DialogFooterVariants = RecipeVariants;
+export type DialogTitleVariants = RecipeVariants;
+export type DialogDescriptionVariants = RecipeVariants;
diff --git a/packages/@luke-ui/react/src/recipes/index.ts b/packages/@luke-ui/react/src/recipes/index.ts
index f9eba261..f9e7d7b1 100644
--- a/packages/@luke-ui/react/src/recipes/index.ts
+++ b/packages/@luke-ui/react/src/recipes/index.ts
@@ -1,5 +1,25 @@
export type { ButtonVariants } from '../recipes/button.css.js';
export { button } from '../recipes/button.css.js';
+export type {
+ DialogCloseButtonVariants,
+ DialogContentVariants,
+ DialogDescriptionVariants,
+ DialogFooterVariants,
+ DialogHeaderVariants,
+ DialogOverlayVariants,
+ DialogTitleVariants,
+ DialogVariants,
+} from '../recipes/dialog.css.js';
+export {
+ dialog,
+ dialogCloseButton,
+ dialogContent,
+ dialogDescription,
+ dialogFooter,
+ dialogHeader,
+ dialogOverlay,
+ dialogTitle,
+} from '../recipes/dialog.css.js';
export { field, fieldLabel, fieldMessage } from '../recipes/field.css.js';
export type { IconVariants } from '../recipes/icon.css.js';
export { icon } from '../recipes/icon.css.js';