Dialog
A modal dialog overlay that blocks interaction with the rest of the page. Supports focus trapping, keyboard dismiss, accessibility, multiple sizes, and composable header/body/footer layout.
The Dialog component provides a modal overlay that blocks interaction with the rest of the page. Built on React Aria's Dialog, Modal, and ModalOverlay primitives with full keyboard navigation, focus trapping, and screen reader support.
Basic Usage
A simple dialog with a trigger button, header, body, and footer. Click outside or press Escape to dismiss.
"use client";
import { Button } from "@repo/ui/components/button";
import {
DialogTrigger,
DialogOverlay,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogBody,
DialogFooter,
DialogClose,
} from "@repo/ui/components/dialog";
/* BASIC USAGE EXAMPLE */
export const Example1 = () => {
return (
<DialogTrigger>
<Button variant="outline">Open Dialog</Button>
<DialogOverlay>
<DialogContent>
<DialogClose />
<DialogHeader>
<DialogTitle>Welcome to FiberUI</DialogTitle>
<DialogDescription>
This is a basic dialog built with React Aria
components. It supports keyboard navigation, focus
trapping, and screen reader accessibility out of the
box.
</DialogDescription>
</DialogHeader>
<DialogBody>
<p className="text-muted-foreground text-sm">
Dialogs are used to display content that requires
user attention or interaction. Click outside or
press Escape to dismiss.
</p>
</DialogBody>
<DialogFooter>
<DialogClose>
<Button variant="outline">Cancel</Button>
</DialogClose>
<DialogClose>
<Button>Continue</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</DialogOverlay>
</DialogTrigger>
);
};
Confirmation Dialog
A destructive confirmation dialog for dangerous actions. Uses size="sm" to keep the dialog compact.
"use client";
import { Button } from "@repo/ui/components/button";
import {
DialogTrigger,
DialogOverlay,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogBody,
DialogFooter,
DialogClose,
} from "@repo/ui/components/dialog";
/* CONFIRMATION DIALOG EXAMPLE */
export const Example2 = () => {
return (
<DialogTrigger>
<Button variant="destructive">Delete Account</Button>
<DialogOverlay>
<DialogContent size="sm">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently
delete your account and remove your data from our
servers.
</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="border-destructive/20 bg-destructive/5 rounded-md border p-3">
<p className="text-destructive text-sm font-medium">
Warning: All your projects, files, and settings
will be permanently removed.
</p>
</div>
</DialogBody>
<DialogFooter>
<DialogClose>
<Button variant="outline">Cancel</Button>
</DialogClose>
<DialogClose>
<Button variant="destructive">
Yes, delete account
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</DialogOverlay>
</DialogTrigger>
);
};
Form Dialog
Embed form elements inside a dialog. All inputs receive proper focus management automatically.
"use client";
import { Button } from "@repo/ui/components/button";
import { Input } from "@repo/ui/components/input";
import { Label } from "@repo/ui/components/label";
import {
DialogTrigger,
DialogOverlay,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogBody,
DialogFooter,
DialogClose,
} from "@repo/ui/components/dialog";
/* FORM DIALOG EXAMPLE */
export const Example3 = () => {
return (
<DialogTrigger>
<Button>Edit Profile</Button>
<DialogOverlay>
<DialogContent>
<DialogClose />
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when
you're done.
</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
placeholder="Enter your name"
defaultValue="Rajat Verma"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="Enter your email"
defaultValue="rajat@fiberui.com"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
placeholder="Enter username"
defaultValue="@rajatverma"
/>
</div>
</div>
</DialogBody>
<DialogFooter>
<DialogClose>
<Button variant="outline">Cancel</Button>
</DialogClose>
<DialogClose>
<Button>Save Changes</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</DialogOverlay>
</DialogTrigger>
);
};
Sizes
The DialogContent component supports sm, default, lg, and full size variants.
"use client";
import { Button } from "@repo/ui/components/button";
import {
DialogTrigger,
DialogOverlay,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
} from "@repo/ui/components/dialog";
/* SIZE VARIANTS EXAMPLE */
export const Example4 = () => {
return (
<div className="flex flex-wrap gap-3">
<DialogTrigger>
<Button variant="outline" size="sm">
Small
</Button>
<DialogOverlay>
<DialogContent size="sm">
<DialogClose />
<DialogHeader>
<DialogTitle>Small Dialog</DialogTitle>
<DialogDescription>
This dialog uses the "sm" size variant
with a max-width of 24rem.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose>
<Button variant="outline" size="sm">
Close
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</DialogOverlay>
</DialogTrigger>
<DialogTrigger>
<Button variant="outline" size="sm">
Default
</Button>
<DialogOverlay>
<DialogContent size="default">
<DialogClose />
<DialogHeader>
<DialogTitle>Default Dialog</DialogTitle>
<DialogDescription>
This dialog uses the "default" size
variant with a max-width of 32rem.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose>
<Button variant="outline" size="sm">
Close
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</DialogOverlay>
</DialogTrigger>
<DialogTrigger>
<Button variant="outline" size="sm">
Large
</Button>
<DialogOverlay>
<DialogContent size="lg">
<DialogClose />
<DialogHeader>
<DialogTitle>Large Dialog</DialogTitle>
<DialogDescription>
This dialog uses the "lg" size variant
with a max-width of 42rem. Ideal for more
complex content.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose>
<Button variant="outline" size="sm">
Close
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</DialogOverlay>
</DialogTrigger>
</div>
);
};
Scrollable Content
When content exceeds the viewport, the DialogBody automatically becomes scrollable while the header and footer remain fixed.
"use client";
import { Button } from "@repo/ui/components/button";
import {
DialogTrigger,
DialogOverlay,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogBody,
DialogFooter,
DialogClose,
} from "@repo/ui/components/dialog";
/* SCROLLABLE CONTENT EXAMPLE */
export const Example5 = () => {
return (
<DialogTrigger>
<Button variant="outline">Terms of Service</Button>
<DialogOverlay>
<DialogContent>
<DialogClose />
<DialogHeader>
<DialogTitle>Terms of Service</DialogTitle>
<DialogDescription>
Please read our terms of service carefully before
proceeding.
</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="text-muted-foreground space-y-4 text-sm">
<div>
<h4 className="text-foreground mb-1 font-semibold">
1. Acceptance of Terms
</h4>
<p>
By accessing and using this service, you
accept and agree to be bound by the terms
and provisions of this agreement. If you do
not agree to abide by the above, please do
not use this service.
</p>
</div>
<div>
<h4 className="text-foreground mb-1 font-semibold">
2. Use License
</h4>
<p>
Permission is granted to temporarily use the
materials on this website for personal,
non-commercial transitory viewing only. This
is the grant of a license, not a transfer of
title.
</p>
</div>
<div>
<h4 className="text-foreground mb-1 font-semibold">
3. Disclaimer
</h4>
<p>
The materials on this website are provided
on an "as is" basis. We make no
warranties, expressed or implied, and hereby
disclaim and negate all other warranties
including, without limitation, implied
warranties or conditions of merchantability.
</p>
</div>
<div>
<h4 className="text-foreground mb-1 font-semibold">
4. Limitations
</h4>
<p>
In no event shall the company or its
suppliers be liable for any damages
(including, without limitation, damages for
loss of data or profit) arising out of the
use or inability to use the materials on
this website.
</p>
</div>
<div>
<h4 className="text-foreground mb-1 font-semibold">
5. Accuracy of Materials
</h4>
<p>
The materials appearing on this website
could include technical, typographical, or
photographic errors. The company does not
warrant that any of the materials on its
website are accurate, complete or current.
</p>
</div>
<div>
<h4 className="text-foreground mb-1 font-semibold">
6. Links
</h4>
<p>
The company has not reviewed all of the
sites linked to its website and is not
responsible for the contents of any such
linked site. The inclusion of any link does
not imply endorsement by the company of the
site.
</p>
</div>
<div>
<h4 className="text-foreground mb-1 font-semibold">
7. Modifications
</h4>
<p>
The company may revise these terms of
service at any time without notice. By using
this website you are agreeing to be bound by
the then current version of these terms of
service.
</p>
</div>
</div>
</DialogBody>
<DialogFooter>
<DialogClose>
<Button variant="outline">Decline</Button>
</DialogClose>
<DialogClose>
<Button>Accept</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</DialogOverlay>
</DialogTrigger>
);
};
Alert Dialog
Set role="alertdialog" and isDismissable={false} on the overlay to create a non-dismissable alert that requires explicit user action.
"use client";
import { Button } from "@repo/ui/components/button";
import {
DialogTrigger,
DialogOverlay,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
} from "@repo/ui/components/dialog";
/* ALERT DIALOG EXAMPLE */
export const Example6 = () => {
return (
<DialogTrigger>
<Button variant="outline">Show Alert</Button>
<DialogOverlay isDismissable={false}>
<DialogContent role="alertdialog" size="sm">
<DialogHeader>
<DialogTitle>Session Expired</DialogTitle>
<DialogDescription>
Your session has expired due to inactivity. Please
sign in again to continue using the application.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose>
<Button>Sign In Again</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</DialogOverlay>
</DialogTrigger>
);
};
Nested Dialog
Dialogs can be nested — opening a second dialog from within the first. Each dialog manages its own focus trap independently.
"use client";
import { Button } from "@repo/ui/components/button";
import {
DialogTrigger,
DialogOverlay,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogBody,
DialogFooter,
DialogClose,
} from "@repo/ui/components/dialog";
/* NESTED DIALOG EXAMPLE */
export const Example7 = () => {
return (
<DialogTrigger>
<Button variant="outline">Open Settings</Button>
<DialogOverlay>
<DialogContent>
<DialogClose />
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>
Manage your application settings. Some actions may
require additional confirmation.
</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="space-y-3">
<div className="flex items-center justify-between rounded-md border p-3">
<div>
<p className="text-sm font-medium">
Notifications
</p>
<p className="text-muted-foreground text-xs">
Enable push notifications
</p>
</div>
<Button variant="outline" size="sm">
Configure
</Button>
</div>
<div className="flex items-center justify-between rounded-md border p-3">
<div>
<p className="text-sm font-medium">
Privacy
</p>
<p className="text-muted-foreground text-xs">
Manage your data and privacy
</p>
</div>
<Button variant="outline" size="sm">
Manage
</Button>
</div>
<div className="border-destructive/20 flex items-center justify-between rounded-md border p-3">
<div>
<p className="text-destructive text-sm font-medium">
Danger Zone
</p>
<p className="text-muted-foreground text-xs">
Irreversible actions
</p>
</div>
<DialogTrigger>
<Button variant="destructive" size="sm">
Delete
</Button>
<DialogOverlay>
<DialogContent size="sm">
<DialogHeader>
<DialogTitle>
Confirm Deletion
</DialogTitle>
<DialogDescription>
This is a nested dialog. Are
you sure you want to proceed
with this destructive
action?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose>
<Button variant="outline">
Go Back
</Button>
</DialogClose>
<DialogClose>
<Button variant="destructive">
Confirm Delete
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</DialogOverlay>
</DialogTrigger>
</div>
</div>
</DialogBody>
<DialogFooter>
<DialogClose>
<Button variant="outline">Close</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</DialogOverlay>
</DialogTrigger>
);
};
Component Code
"use client";
import * as React from "react";
import { XIcon } from "lucide-react";
import {
DialogTrigger as AriaDialogTrigger,
Modal as AriaModal,
ModalOverlay as AriaModalOverlay,
Dialog as AriaDialog,
Heading as AriaHeading,
Button as AriaButton,
composeRenderProps,
type DialogTriggerProps as AriaDialogTriggerProps,
type ModalOverlayProps as AriaModalOverlayProps,
type DialogProps as AriaDialogProps,
type HeadingProps as AriaHeadingProps,
type ButtonProps as AriaButtonProps,
} from "react-aria-components";
import { cn, tv, type VariantProps } from "tailwind-variants";
/* -----------------------------------------------------------------------------
* DialogTrigger (Root)
* ---------------------------------------------------------------------------*/
interface DialogTriggerProps extends AriaDialogTriggerProps {}
export const DialogTrigger = (props: DialogTriggerProps) => {
return <AriaDialogTrigger {...props} />;
};
/* -----------------------------------------------------------------------------
* DialogOverlay
* ---------------------------------------------------------------------------*/
const overlayStyles = tv({
base: [
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm",
"flex items-center justify-center",
],
variants: {
isEntering: {
true: "animate-in fade-in-0 duration-200",
},
isExiting: {
true: "animate-out fade-out-0 duration-150",
},
},
});
interface DialogOverlayProps extends AriaModalOverlayProps {
children: React.ReactNode;
}
export const DialogOverlay = ({
className,
children,
isDismissable = true,
...props
}: DialogOverlayProps) => {
return (
<AriaModalOverlay
data-slot="dialog-overlay"
isDismissable={isDismissable}
className={composeRenderProps(
className,
(className, renderProps) =>
cn(overlayStyles({ ...renderProps }), className) || "",
)}
{...props}
>
<AriaModal data-slot="dialog-modal" className="outline-none">
{children}
</AriaModal>
</AriaModalOverlay>
);
};
/* -----------------------------------------------------------------------------
* DialogContent
* ---------------------------------------------------------------------------*/
const contentStyles = tv({
base: [
"bg-background text-foreground relative flex flex-col rounded-lg border shadow-lg outline-none",
"max-h-[85vh] w-full",
],
variants: {
size: {
sm: "max-w-sm",
default: "max-w-lg",
lg: "max-w-2xl",
full: "h-[calc(100vh-2rem)] max-w-[calc(100vw-2rem)]",
},
isEntering: {
true: "animate-in fade-in-0 zoom-in-95 slide-in-from-bottom-2 duration-200",
},
isExiting: {
true: "animate-out fade-out-0 zoom-out-95 slide-out-to-bottom-2 duration-150",
},
},
defaultVariants: {
size: "default",
},
});
interface DialogContentProps
extends AriaDialogProps,
VariantProps<typeof contentStyles> {}
export const DialogContent = ({
className,
size,
children,
...props
}: DialogContentProps) => {
return (
<AriaDialog
data-slot="dialog-content"
className={cn(contentStyles({ size }), className)}
{...props}
>
{children}
</AriaDialog>
);
};
/* -----------------------------------------------------------------------------
* DialogHeader
* ---------------------------------------------------------------------------*/
interface DialogHeaderProps extends React.ComponentProps<"div"> {}
export const DialogHeader = ({
className,
children,
...props
}: DialogHeaderProps) => {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-1.5 p-6 pb-0", className)}
{...props}
>
{children}
</div>
);
};
/* -----------------------------------------------------------------------------
* DialogTitle
* ---------------------------------------------------------------------------*/
interface DialogTitleProps extends AriaHeadingProps {}
export const DialogTitle = ({ className, ...props }: DialogTitleProps) => {
return (
<AriaHeading
data-slot="dialog-title"
slot="title"
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
);
};
/* -----------------------------------------------------------------------------
* DialogDescription
* ---------------------------------------------------------------------------*/
interface DialogDescriptionProps extends React.ComponentProps<"p"> {}
export const DialogDescription = ({
className,
...props
}: DialogDescriptionProps) => {
return (
<p
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
};
/* -----------------------------------------------------------------------------
* DialogBody
* ---------------------------------------------------------------------------*/
interface DialogBodyProps extends React.ComponentProps<"div"> {}
export const DialogBody = ({
className,
children,
...props
}: DialogBodyProps) => {
return (
<div
data-slot="dialog-body"
className={cn("flex-1 overflow-y-auto p-6", className)}
{...props}
>
{children}
</div>
);
};
/* -----------------------------------------------------------------------------
* DialogFooter
* ---------------------------------------------------------------------------*/
interface DialogFooterProps extends React.ComponentProps<"div"> {}
export const DialogFooter = ({
className,
children,
...props
}: DialogFooterProps) => {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 p-6 pt-0 sm:flex-row sm:justify-end",
className,
)}
{...props}
>
{children}
</div>
);
};
/* -----------------------------------------------------------------------------
* DialogClose
* ---------------------------------------------------------------------------*/
interface DialogCloseProps extends AriaButtonProps {}
export const DialogClose = ({
className,
children,
...props
}: DialogCloseProps) => {
if (children) {
return (
<AriaButton
data-slot="dialog-close"
slot="close"
className={cn(className)}
{...props}
>
{children}
</AriaButton>
);
}
return (
<AriaButton
data-slot="dialog-close"
slot="close"
className={cn(
"ring-offset-background focus:ring-ring absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none",
className,
)}
{...props}
>
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</AriaButton>
);
};
ComboBox
A combo box combines a text input with a listbox, allowing users to filter a list of options to items matching a query. Supports sections, custom items, async loading, and form integration.
GridList
An interactive list component that supports keyboard navigation, single and multiple selection, disabled items, and custom content rendering. Built on React Aria for full accessibility.