The Dialog component extends Base UI's Dialog with polished default styles and structural subcomponents: Dialog.Header, Dialog.Body, and Dialog.Footer. Set size on Dialog.Root (sm, md, lg, xl) to pick a popup width preset. Unlike AlertDialog (which uses role="alertdialog" for confirmations), Dialog uses role="dialog" and closes on outside click or Escape by default.
Here is the component implementation code that you can copy to your project:
// https://devie-ui.com/components/dialog
// https://base-ui.com/react/components/dialog
"use client";
import { Dialog as BaseDialog } from "@base-ui/react/dialog";
import clsx from "clsx";
import * as React from "react";
import styles from "./Dialog.module.scss";
export type DialogSize = "sm" | "md" | "lg" | "xl";
const DialogInteractionContext = React.createContext<boolean | null>(null);
const DialogSizeContext = React.createContext<DialogSize>("md");
function Root({
disableInteractions,
size = "md",
...props
}: Dialog.Root.Props) {
return (
<DialogSizeContext.Provider value={size}>
<DialogInteractionContext.Provider value={disableInteractions ?? null}>
<BaseDialog.Root {...props} />
</DialogInteractionContext.Provider>
</DialogSizeContext.Provider>
);
}
const createHandle = BaseDialog.createHandle;
function Trigger({ className, ...props }: BaseDialog.Trigger.Props) {
return (
<BaseDialog.Trigger
className={clsx(styles.trigger, className)}
{...props}
/>
);
}
const Portal = BaseDialog.Portal;
function Close({ className, render, ...props }: BaseDialog.Close.Props) {
return (
<BaseDialog.Close
className={clsx(!render && styles.close, className)}
render={render}
{...props}
/>
);
}
function Backdrop({ className, ...props }: BaseDialog.Backdrop.Props) {
return (
<BaseDialog.Backdrop
className={clsx(styles.backdrop, className)}
{...props}
/>
);
}
function Popup({
className,
disableInteractions,
...props
}: Dialog.Popup.Props) {
const size = React.useContext(DialogSizeContext);
const contextDisableInteractions = React.useContext(DialogInteractionContext);
const resolvedDisableInteractions =
disableInteractions ?? contextDisableInteractions ?? false;
return (
<BaseDialog.Popup
className={clsx(
styles.popup,
size === "sm" && styles.popupSm,
size === "md" && styles.popupMd,
size === "lg" && styles.popupLg,
size === "xl" && styles.popupXl,
resolvedDisableInteractions && styles.popupNonInteractive,
className,
)}
aria-busy={resolvedDisableInteractions ? true : undefined}
inert={resolvedDisableInteractions ? true : undefined}
{...props}
/>
);
}
function Title({ className, ...props }: BaseDialog.Title.Props) {
return (
<BaseDialog.Title
className={clsx(styles.title, className)}
// biome-ignore lint/a11y/useHeadingContent: Base UI Title renders children into this heading
render={<h3 />}
{...props}
/>
);
}
function Description({ className, ...props }: BaseDialog.Description.Props) {
return (
<BaseDialog.Description
className={clsx(styles.description, className)}
{...props}
/>
);
}
function Header({ className, ...props }: Dialog.Header.Props) {
return <div className={clsx(styles.header, className)} {...props} />;
}
function Footer({ className, ...props }: Dialog.Footer.Props) {
return <div className={clsx(styles.footer, className)} {...props} />;
}
function Body({ className, ...props }: Dialog.Body.Props) {
return <div className={clsx(styles.body, className)} {...props} />;
}
// biome-ignore lint/correctness/noUnusedVariables: merged with `namespace Dialog` for compound export
const Dialog = {
Root,
createHandle,
Trigger,
Portal,
Close,
Backdrop,
Popup,
Header,
Footer,
Title,
Description,
Body,
};
namespace Dialog {
export namespace Root {
export interface Props extends BaseDialog.Root.Props {
disableInteractions?: boolean;
size?: DialogSize;
}
}
export namespace Trigger {
export type Props = BaseDialog.Trigger.Props;
}
export namespace Portal {
export type Props = BaseDialog.Portal.Props;
}
export namespace Close {
export type Props = BaseDialog.Close.Props;
}
export namespace Backdrop {
export type Props = BaseDialog.Backdrop.Props;
}
export namespace Popup {
export interface Props extends BaseDialog.Popup.Props {
disableInteractions?: boolean;
}
}
export namespace Title {
export type Props = BaseDialog.Title.Props;
}
export namespace Description {
export type Props = BaseDialog.Description.Props;
}
export namespace Header {
export interface Props extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
}
}
export namespace Footer {
export interface Props extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
}
}
export namespace Body {
export interface Props extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
}
}
}
export default Dialog;A basic dialog with a title, description, and action buttons. Use Dialog.Trigger to open and Dialog.Close to dismiss.
Use the size prop on Dialog.Root to choose a popup width. The default is md.
Use open and onOpenChange to control the dialog programmatically without a Dialog.Trigger.
Use a nested dialog for a short, tightly related subtask when closing the parent would discard useful context. Place the child Dialog.Root inside the parent root and usually give it a narrower size.
Base UI's nested dialog guidance explains that a child backdrop is not rendered. This component keeps the single page backdrop, but applies the same 70% darkening to the entire parent popup through data-nested-dialog-open. The composed popup—including its content, border, and shadow—also shrinks by 4% per nested level without compounding the backdrop over the whole viewport.
Best practice: keep the stack to two layers, give every dialog its own title and visible close or cancel action, and let Base UI manage focus, Escape, and stacking. Use an AlertDialog for an irreversible confirmation. For a long or independent flow, prefer a page or replace the parent dialog instead of adding another layer. The WAI-ARIA modal dialog pattern similarly requires that only the active dialog is interactive, focus stays inside it, and focus returns to its invoking control when it closes.