A dialog is an overlay shown above other content in an application.
"use client";
import React from "react";
import { Button } from "@/lib/components/core/default/button";
import { DialogRoot, Dialog, DialogBody, DialogFooter } from "@/lib/components/core/default/dialog";
import { TextField } from "@/lib/components/core/default/text-field";
function Demo() {
return (
<DialogRoot>
<Button variant="outline">Edit Profile</Button>
<Dialog title="Edit profile" description="Make changes to your profile.">
{({ close }) => (
<>
<DialogBody>
<TextField autoFocus label="Name" defaultValue="Mehdi" />
<TextField label="Username" defaultValue="@mehdibha_" />
</DialogBody>
<DialogFooter>
<Button variant="outline" size={{ initial: "lg", sm: "md" }} onPress={close}>
Cancel
</Button>
<Button variant="primary" size={{ initial: "lg", sm: "md" }} onPress={close}>
Save changes
</Button>
</DialogFooter>
</>
)}
</Dialog>
</DialogRoot>
);
}
Installation
Copy and paste the following code into your project.
"use client";
import * as React from "react";
import { useSlotId } from "@react-aria/utils";
import {
composeRenderProps,
DialogContext as AriaDialogContext,
DialogTrigger as AriaDialogTrigger,
Dialog as AriaDialog,
Heading as AriaHeading,
Text as AriaText,
type DialogProps as AriaDialogProps,
type DialogTriggerProps as AriaDialogTriggerProps,
type HeadingProps as AriaHeadingProps,
type TextProps as AriaTextProps,
TextContext,
} from "react-aria-components";
import { Provider } from "react-aria-components";
import { tv } from "tailwind-variants";
import { Overlay, type OverlayProps } from "./overlay";
const dialogStyles = tv({
slots: {
overlay: "",
backdrop: "",
content: "relative outline-none rounded-[inherit] p-4 flex flex-col max-w-full",
header: "mb-4",
title: "text-xl font-semibold",
description: "text-sm text-fg-muted",
body: "space-y-4",
footer: "flex flex-col-reverse sm:flex-row sm:justify-end gap-2 pt-4",
inset: "-mx-3 sm:-mx-6",
},
});
type DialogRootProps = AriaDialogTriggerProps;
const DialogRoot = (props: DialogRootProps) => {
const descriptionId = useSlotId();
return (
<Provider
values={[
[AriaDialogContext, { "aria-describedby": descriptionId }],
[TextContext, { slots: { description: { id: descriptionId } } }],
]}
>
<AriaDialogTrigger {...props} />
</Provider>
);
};
interface DialogProps extends DialogContentProps {
title?: string;
description?: string;
type?: OverlayProps["type"];
mobileType?: OverlayProps["mobileType"];
mediaQuery?: OverlayProps["mediaQuery"];
isDismissable?: boolean;
}
const Dialog = ({
title,
description,
type,
mobileType,
mediaQuery,
isDismissable: isDismissableProp,
...props
}: DialogProps) => {
const isDismissable = isDismissableProp ?? (props.role === "alertdialog" ? false : true);
return (
<Overlay
isDismissable={isDismissable}
type={type}
mobileType={mobileType}
mediaQuery={mediaQuery}
>
<DialogContent {...props}>
{composeRenderProps(props.children, (children) => (
<>
{(title || description) && (
<DialogHeader>
{title && <DialogTitle>{title}</DialogTitle>}
{description && <DialogDescription>{description}</DialogDescription>}
</DialogHeader>
)}
{children}
</>
))}
</DialogContent>
</Overlay>
);
};
type DialogContentProps = AriaDialogProps;
const DialogContent = ({ children, className, ...props }: DialogContentProps) => {
const { content } = dialogStyles();
return (
<AriaDialog className={content({ className })} {...props}>
{children}
</AriaDialog>
);
};
type DialogHeaderProps = React.ComponentProps<"header">;
const DialogHeader = ({ children, className, ...props }: DialogHeaderProps) => {
const { header } = dialogStyles();
return (
<header className={header({ className })} {...props}>
{children}
</header>
);
};
type DialogTitleProps = AriaHeadingProps;
const DialogTitle = ({ className, ...props }: DialogTitleProps) => {
const { title } = dialogStyles();
return <AriaHeading slot="title" className={title({ className })} {...props} />;
};
type DialogDescriptionProps = AriaTextProps;
const DialogDescription = ({ className, ...props }: DialogDescriptionProps) => {
const { description } = dialogStyles();
return <AriaText slot="description" className={description({ className })} {...props} />;
};
type DialogBody = React.ComponentProps<"div">;
const DialogBody = ({ className, ...props }: DialogDescriptionProps) => {
const { body } = dialogStyles();
return <div className={body({ className })} {...props} />;
};
type DialogFooterProps = React.ComponentProps<"footer">;
const DialogFooter = ({ className, ...props }: DialogFooterProps) => {
const { footer } = dialogStyles();
return <footer slot="description" className={footer({ className })} {...props} />;
};
type DialogInsetProps = React.ComponentProps<"div">;
const DialogInset = ({ className, ...props }: DialogInsetProps) => {
const { inset } = dialogStyles();
return <div className={inset({ className })} {...props} />;
};
export type {
DialogRootProps,
DialogProps,
DialogContentProps,
DialogHeaderProps,
DialogTitleProps,
DialogDescriptionProps,
DialogFooterProps,
DialogInsetProps,
};
export {
DialogRoot,
DialogContent,
Dialog,
DialogHeader,
DialogTitle,
DialogDescription,
DialogBody,
DialogFooter,
DialogInset,
};
Update the import paths to match your project setup.
Usage
Use dialog to display contextual information, tasks, or workflows that appear over the user interface. Depending on the kind of dialog, further interactions may be blocked until the dialog is acknowledged.
Options
Title
Dialog is labeled with a title to provide context to the user. Use the title prop to provide a title for the dialog.
If a dialog does not have a visible heading element, an aria-label or aria-labelledby prop must be passed instead to identify the element to assistive technology.
import React from "react";
import { Button } from "@/lib/components/core/default/button";
import { DialogRoot, Dialog } from "@/lib/components/core/default/dialog";
import { TextField } from "@/lib/components/core/default/text-field";
function Demo() {
return (
<DialogRoot>
<Button variant="outline">Edit username</Button>
<Dialog title="Edit username">
<TextField label="Username" defaultValue="@mehdibha_" />
</Dialog>
</DialogRoot>
);
}
Description
A description can be supplied to the dialog via the description prop.
import React from "react";
import { Button } from "@/lib/components/core/default/button";
import { DialogRoot, Dialog } from "@/lib/components/core/default/dialog";
import { TextField } from "@/lib/components/core/default/text-field";
function Demo() {
return (
<DialogRoot>
<Button variant="outline">Edit username</Button>
<Dialog title="Edit username">
<TextField label="Username" defaultValue="@mehdibha_" />
</Dialog>
</DialogRoot>
);
}
Type
Dialogs can be rendered as modals, popovers, or drawers.
import React from "react";
import { Button } from "@/lib/components/core/default/button";
import { DialogRoot, Dialog } from "@/lib/components/core/default/dialog";
import { TextField } from "@/lib/components/core/default/text-field";
function Demo() {
return (
<DialogRoot>
<Button variant="outline">Edit username</Button>
<Dialog title="Edit username">
<TextField label="Username" defaultValue="@mehdibha_" />
</Dialog>
</DialogRoot>
);
}
Mobile type
By default, dialogs are displayed as drawers on mobile. You can override this behavior by setting the mobileType prop.
import React from "react";
import { Button } from "@/lib/components/core/default/button";
import { DialogRoot, Dialog } from "@/lib/components/core/default/dialog";
import { TextField } from "@/lib/components/core/default/text-field";
function Demo() {
return (
<DialogRoot>
<Button variant="outline">Edit username</Button>
<Dialog title="Edit username">
<TextField label="Username" defaultValue="@mehdibha_" />
</Dialog>
</DialogRoot>
);
}
Dismissable
By default, dialogs are dismissable allowing users to click outside to close the dialog. You can disable this behavior by setting the isDismissable prop.
"use client";
import React from "react";
import { Button } from "@/lib/components/core/default/button";
import { DialogRoot, Dialog, DialogBody, DialogFooter } from "@/lib/components/core/default/dialog";
import { TextField } from "@/lib/components/core/default/text-field";
function Demo() {
return (
<DialogRoot>
<Button variant="outline">Edit Profile</Button>
<Dialog
title="Edit profile"
description="Make changes to your profile."
isDismissable={false}
>
{({ close }) => (
<>
<DialogBody>
<TextField autoFocus label="Name" defaultValue="Mehdi" />
<TextField label="Username" defaultValue="@mehdibha_" />
</DialogBody>
<DialogFooter>
<Button variant="outline" size={{ initial: "lg", sm: "md" }} onPress={close}>
Cancel
</Button>
<Button variant="primary" size={{ initial: "lg", sm: "md" }} onPress={close}>
Save changes
</Button>
</DialogFooter>
</>
)}
</Dialog>
</DialogRoot>
);
}
By default, dialogs have a dismiss button when they're dismissable. You can disable this behavior by setting showDismissButton prop.
Inset content
Use the <DialogInset> component to add inset content to the dialog.
import React from "react";
function Demo() {
return <p className="text-3xl">TODO</p>;
}
Popover
Use the type="popover" prop on the <Dialog> element to make a popover dialog.
"use client";
import React from "react";
import type { Key } from "react-aria-components";
import { Button } from "@/lib/components/core/default/button";
import { DialogRoot, Dialog } from "@/lib/components/core/default/dialog";
import { Item } from "@/lib/components/core/default/list-box";
import { NumberField } from "@/lib/components/core/default/number-field";
import { Select } from "@/lib/components/core/default/select";
import { Switch } from "@/lib/components/core/default/switch";
import { InfoIcon } from "@/lib/icons";
function Demo() {
const [placement, setPlacement] = React.useState<Key>("top");
const [offset, setOffset] = React.useState<number>(0);
const [crossOffset, setCrossOffset] = React.useState<number>(0);
const [containerPadding, setContainerPadding] = React.useState<number>(0);
const [showArrow, setShowArrow] = React.useState<boolean>(true);
return (
<div className="flex w-full items-center">
<div className="flex flex-1 items-center justify-center">
<DialogRoot>
<Button variant="outline" shape="square">
<InfoIcon />
</Button>
<Dialog
type="popover"
title="Help"
description="For help accessing your account, please contact support."
/>
</DialogRoot>
</div>
<div className="space-y-4 rounded-md border p-4">
<Select label="Placement" selectedKey={placement} onSelectionChange={setPlacement}>
<Item id="top">Top</Item>
<Item id="bottom">Bottom</Item>
</Select>
<NumberField label="Offset" value={offset} onChange={setOffset} />
<NumberField label="Cross offset" value={crossOffset} onChange={setCrossOffset} />
<NumberField
label="Container padding"
value={containerPadding}
onChange={setContainerPadding}
/>
<Switch isSelected={showArrow} onChange={setShowArrow}>
Arrow
</Switch>
</div>
</div>
);
}
Drawer
Use the type="drawer" prop on the <Dialog> element to make a drawer dialog.
"use client";
import React from "react";
import type { Key } from "react-aria-components";
import { Button } from "@/lib/components/core/default/button";
import { DialogRoot, Dialog } from "@/lib/components/core/default/dialog";
import { Item } from "@/lib/components/core/default/list-box";
import { Select } from "@/lib/components/core/default/select";
import { Switch } from "@/lib/components/core/default/switch";
function Demo() {
const [placement, setPlacement] = React.useState<Key>("top");
const [swipeable, setSwipeable] = React.useState<boolean>(true);
return (
<div className="flex w-full items-center">
<div className="flex flex-1 items-center justify-center">
<DialogRoot>
<Button variant="outline">Open drawer</Button>
<Dialog
type="drawer"
title="Help"
description="For help accessing your account, please contact support."
/>
</DialogRoot>
</div>
<div className="space-y-4 rounded-md border p-4">
<Select label="Placement" selectedKey={placement} onSelectionChange={setPlacement}>
<Item id="top">Top</Item>
<Item id="bottom">Bottom</Item>
</Select>
<Switch isSelected={swipeable} onChange={setSwipeable}>
Swipeable
</Switch>
</div>
</div>
);
}
Alert Dialog
Use the role="alertdialog" prop on the <Dialog> element to make an alert dialog.
If the isDismissable prop is not explicitly set, the dialog will be not dismissable.
"use client";
import React from "react";
import { Button } from "@/lib/components/core/default/button";
import { DialogRoot, Dialog, DialogFooter } from "@/lib/components/core/default/dialog";
function Demo() {
return (
<DialogRoot>
<Button variant="danger">Delete project</Button>
<Dialog
title="Delete project"
description="Are you sure you want to delete this project? This action is permanent and cannot be undone."
role="alertdialog"
// isDissmissible={false}
>
{({ close }) => (
<>
<DialogFooter>
<Button variant="outline" size={{ initial: "lg", sm: "md" }} onPress={close}>
Cancel
</Button>
<Button variant="danger" size={{ initial: "lg", sm: "md" }} onPress={close}>
Delete project
</Button>
</DialogFooter>
</>
)}
</Dialog>
</DialogRoot>
);
}
Controlled
Use the isOpen and onOpenChange props to control the dialog's open state.
"use client";
import React from "react";
import { Button } from "@/lib/components/core/default/button";
import { DialogRoot, Dialog, DialogBody, DialogFooter } from "@/lib/components/core/default/dialog";
import { TextField } from "@/lib/components/core/default/text-field";
function Demo() {
return (
<DialogRoot>
<Button variant="outline">Edit Profile</Button>
<Dialog title="Edit profile" description="Make changes to your profile.">
{({ close }) => (
<>
<DialogBody>
<TextField autoFocus label="Name" defaultValue="Mehdi" />
<TextField label="Username" defaultValue="@mehdibha_" />
</DialogBody>
<DialogFooter>
<Button variant="outline" size={{ initial: "lg", sm: "md" }} onPress={close}>
Cancel
</Button>
<Button variant="primary" size={{ initial: "lg", sm: "md" }} onPress={close}>
Save changes
</Button>
</DialogFooter>
</>
)}
</Dialog>
</DialogRoot>
);
}
Composition
If you need to customize things further, you can drop down to the composition level.
import React from "react";
import { Button } from "@/lib/components/core/default/button";
import { DialogRoot, Dialog } from "@/lib/components/core/default/dialog";
import { TextField } from "@/lib/components/core/default/text-field";
function Demo() {
return (
<DialogRoot>
<Button variant="outline">Edit username</Button>
<Dialog title="Edit username">
<TextField label="Username" defaultValue="@mehdibha_" />
</Dialog>
</DialogRoot>
);
}
Examples
async-form-submission.tsx
Nested dialog
Dialogs support nesting, allowing you to open a dialog from within another dialog.
"use client";
import React from "react";
import { Button } from "@/lib/components/core/default/button";
import { DialogRoot, Dialog } from "@/lib/components/core/default/dialog";
import { Radio, RadioGroup } from "@/lib/components/core/default/radio-group";
type Type = "modal" | "drawer" | "popover";
function Demo() {
const [type, setType] = React.useState<Type>("modal");
return (
<div className="flex w-full items-center">
<div className="flex flex-1 items-center justify-center">
<DialogRoot>
<Button variant="outline">Dialog</Button>
<Dialog title="Dialog" type={type}>
<DialogRoot>
<Button variant="outline">Nested dialog</Button>
<Dialog title="Nested dialog" type={type}></Dialog>
</DialogRoot>
</Dialog>
</DialogRoot>
</div>
<div className="space-y-4 rounded-md border px-10 py-6">
<RadioGroup label="Type" value={type} onChange={(value) => setType(value as Type)}>
<Radio value="modal">Modal</Radio>
<Radio value="drawer">Drawer</Radio>
<Radio value="popover">Popover</Radio>
</RadioGroup>
</div>
</div>
);
}
API Reference
DialogRoot
Prop | Type | Default | Description |
---|
children* | ReactNode | - | |
isOpen | boolean | - | Whether the overlay is open by default (controlled). |
defaultOpen | boolean | - | Whether the overlay is open by default (uncontrolled). |
Event | Type | Description |
---|
onOpenChange | (isOpen: boolean) => void | Handler that is called when the overlay's open state changes. |
Dialog
Prop | Type | Default | Description |
---|
children | ReactNode | (opts: DialogRenderProps) => ReactNode | - | Children of the dialog. A function may be provided to access a function to close the dialog. |
className | string | - | The CSS className for the element. |
style | CSSProperties | - | The inline style for the element. |
Accessibility
Keyboard interactions
Key | Description |
---|
Space Enter | When focus is on the trigger, opens the dialog. |
Tab | Moves focus to the next button inside the dialog (last becomes first). If none of the buttons are selected, the focus is set on the first button. |
Shift+Tab | Moves focus to the previous button inside the dialog (first becomes last). If none of the buttons are selected, the focus is set on the last button. |
Esc | Dismisses the dialog and moves focus to the trigger. |