Dialog
A shadcn-style dialog component built with Ark UI primitives.
Edit profile
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
const DialogBasicDemo = () => (
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
Open dialog
</Button>
</DialogTrigger>
<DialogPopup className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes here. Save when you are done.
</DialogDescription>
</DialogHeader>
<DialogPanel>
<Input defaultValue="Jane Doe" aria-label="Name" />
</DialogPanel>
<DialogFooter>
<DialogClose asChild>
<Button size="sm" variant="ghost">
Cancel
</Button>
</DialogClose>
<Button size="sm" type="button">
Save
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
);
export default DialogBasicDemo;
Installation
npx shadcn@latest add @ark-cn/dialogInstall the dependency required by this primitive:
npm install @ark-ui/react lucide-react class-variance-authorityCopy the component source into your app:
"use client";
import { Dialog as DialogPrimitive } from "@ark-ui/react/dialog";
import { ark } from "@ark-ui/react/factory";
import { Portal } from "@ark-ui/react/portal";
import { cva, type VariantProps } from "class-variance-authority";
import { XIcon } from "lucide-react";
import { type ComponentProps } from "react";
import { cn } from "@/lib/utils";
export const Dialog = (props: DialogPrimitive.RootProps) => (
<DialogPrimitive.Root {...props} />
);
export const DialogBackdrop = ({
className,
...props
}: DialogPrimitive.BackdropProps) => (
<DialogPrimitive.Backdrop
className={cn(
"fixed inset-0 z-[calc(50+var(--layer-index,0))] bg-black/40 backdrop-blur-md transition-[opacity,backdrop-filter] duration-200 dark:bg-black/50 dark:backdrop-blur-lg data-[state=closed]:opacity-0 data-[state=open]:opacity-100 supports-backdrop-filter:bg-black/25 supports-backdrop-filter:dark:bg-black/35",
className,
)}
{...props}
/>
);
const positionerVariants = cva(
"fixed inset-0 z-[calc(50+var(--layer-index,0))] flex overscroll-y-none",
{
defaultVariants: {
bottomStick: true,
},
variants: {
bottomStick: {
false: "items-center justify-center p-4",
true: "items-end justify-center sm:items-center",
},
},
},
);
const contentVariants = cva(
"relative z-[calc(50+var(--layer-index,0))] grid min-h-0 max-h-[min(calc(100dvh-2rem),var(--dialog-max-h,32rem))] w-full shrink-0 gap-4 overflow-x-hidden overflow-y-auto rounded-xl border border-border bg-popover px-6 pt-6 pb-0 text-card-foreground shadow-lg outline-none transition-[opacity,transform] duration-150 data-[state=closed]:scale-[0.98] data-[state=closed]:opacity-0 data-[state=open]:scale-100 data-[state=open]:opacity-100 data-has-nested:data-[state=open]:scale-[calc(1-var(--nested-layer-count,0)*0.05)] max-sm:max-h-[min(calc(100dvh-1rem),var(--dialog-max-h,90dvh))] max-sm:rounded-b-none max-sm:rounded-t-xl sm:rounded-xl",
{
defaultVariants: {
size: "default",
},
variants: {
size: {
default: "max-w-lg",
sm: "max-w-sm gap-3 px-5 pt-5 pb-0 text-sm **:data-[slot=dialog-title]:text-base",
},
},
},
);
export type DialogPopupProps = DialogPrimitive.ContentProps & {
backdropClassName?: string;
bottomStickOnMobile?: boolean;
closeProps?: DialogPrimitive.CloseTriggerProps;
positionerClassName?: string;
showCloseButton?: boolean;
size?: VariantProps<typeof contentVariants>["size"];
};
export const DialogTrigger = ({
className,
...props
}: DialogPrimitive.TriggerProps) => {
return <DialogPrimitive.Trigger className={cn(className)} {...props} />;
};
export const DialogPopup = ({
backdropClassName,
bottomStickOnMobile = true,
children,
className,
closeProps,
positionerClassName,
showCloseButton = true,
size = "default",
...contentProps
}: DialogPopupProps) => {
const {
className: closeClassName,
children: closeChildren,
...closeRest
} = closeProps ?? {};
return (
<Portal>
<DialogBackdrop className={backdropClassName} />
<DialogPrimitive.Positioner
className={cn(
positionerVariants({ bottomStick: bottomStickOnMobile }),
positionerClassName,
)}
>
<DialogPrimitive.Content
className={cn(contentVariants({ size }), className)}
{...contentProps}
>
{showCloseButton ? (
<DialogPrimitive.CloseTrigger
aria-label="Close"
className={cn(
"absolute top-4 inset-e-4 z-10 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
closeClassName,
)}
{...closeRest}
>
{closeChildren ?? <XIcon className="size-4" />}
</DialogPrimitive.CloseTrigger>
) : null}
{children}
</DialogPrimitive.Content>
</DialogPrimitive.Positioner>
</Portal>
);
};
export const DialogHeader = ({
className,
...props
}: ComponentProps<typeof ark.div>) => (
<ark.div
className={cn("flex flex-col gap-2 text-center sm:text-start", className)}
data-slot="dialog-header"
{...props}
/>
);
const footerVariants = cva(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:gap-2",
{
defaultVariants: {
variant: "default",
},
variants: {
variant: {
bare: "pb-4",
default:
"-mx-6 mt-2 border-border border-t bg-muted/40 px-6 py-4 sm:rounded-b-xl max-sm:rounded-b-none",
},
},
},
);
export const DialogFooter = ({
className,
variant,
...props
}: ComponentProps<"div"> & VariantProps<typeof footerVariants>) => (
<ark.div
className={cn(footerVariants({ variant }), className)}
data-slot="dialog-footer"
{...props}
/>
);
export const DialogTitle = ({
className,
...props
}: DialogPrimitive.TitleProps) => (
<DialogPrimitive.Title
className={cn("font-semibold text-lg leading-none", className)}
data-slot="dialog-title"
{...props}
/>
);
export const DialogDescription = ({
className,
...props
}: DialogPrimitive.DescriptionProps) => (
<DialogPrimitive.Description
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
const panelMask =
"supports-[mask-image:linear-gradient(white,transparent)]:[mask-image:linear-gradient(to_bottom,transparent,black_0.75rem,black_calc(100%-0.75rem),transparent)]";
export const DialogPanel = ({
className,
scrollFade = false,
scrollable = false,
...props
}: ComponentProps<typeof ark.div> & {
scrollFade?: boolean;
scrollable?: boolean;
}) => (
<ark.div
className={cn(
"flex min-h-0 w-full flex-1 flex-col gap-4",
scrollable &&
"overflow-y-auto overscroll-contain [scrollbar-gutter:stable]",
scrollFade && panelMask,
className,
)}
data-slot="dialog-panel"
{...props}
/>
);
export const DialogClose = ({
...props
}: DialogPrimitive.CloseTriggerProps) => {
return <DialogPrimitive.CloseTrigger {...props} />;
};
export const DialogRootProvider = ({
...props
}: DialogPrimitive.RootProviderProps) => (
<DialogPrimitive.RootProvider {...props} />
);
export const DialogContext = DialogPrimitive.Context;
export { useDialog, useDialogContext } from "@ark-ui/react/dialog";
Update import aliases to match your project setup.
Usage
import * as Dialog from "@/components/ui/dialog"Read exported parts in src/components/ui/dialog.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Basic
Edit profile
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
const DialogBasicDemo = () => (
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
Open dialog
</Button>
</DialogTrigger>
<DialogPopup className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes here. Save when you are done.
</DialogDescription>
</DialogHeader>
<DialogPanel>
<Input defaultValue="Jane Doe" aria-label="Name" />
</DialogPanel>
<DialogFooter>
<DialogClose asChild>
<Button size="sm" variant="ghost">
Cancel
</Button>
</DialogClose>
<Button size="sm" type="button">
Save
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
);
export default DialogBasicDemo;
Nested
Manage team member
DialogRootProvider trees with useDialog. When the child is open, the parent panel gets data-has-nested and scales down.Name
Bora Baloglu
bora@example.com
Edit details
import { Button } from "@/components/ui/button";
import {
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogRootProvider,
DialogTitle,
useDialog,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
const DialogNestedDemo = () => {
const parentDialog = useDialog();
const childDialog = useDialog();
return (
<>
<Button
size="sm"
type="button"
variant="outline"
onClick={() => parentDialog.setOpen(true)}
>
Open parent
</Button>
<DialogRootProvider value={parentDialog}>
<DialogPopup className="sm:max-w-md" showCloseButton={false}>
<DialogHeader>
<DialogTitle>Manage team member</DialogTitle>
<DialogDescription>
Two sibling{" "}
<code className="rounded bg-muted px-1 py-px text-xs">
DialogRootProvider
</code>{" "}
trees with{" "}
<code className="rounded bg-muted px-1 py-px text-xs">
useDialog
</code>
. When the child is open, the parent panel gets{" "}
<code className="rounded bg-muted px-1 py-px text-xs">
data-has-nested
</code>{" "}
and scales down.
</DialogDescription>
</DialogHeader>
<DialogPanel className="grid gap-4">
<div className="grid gap-1">
<p className="text-muted-foreground text-sm">Name</p>
<p className="font-medium text-sm">Bora Baloglu</p>
</div>
<div className="grid gap-1">
<p className="text-muted-foreground text-sm">Email</p>
<p className="font-medium text-sm">bora@example.com</p>
</div>
</DialogPanel>
<DialogFooter>
<Button
size="sm"
type="button"
variant="outline"
onClick={() => childDialog.setOpen(true)}
>
Edit details
</Button>
</DialogFooter>
</DialogPopup>
</DialogRootProvider>
<DialogRootProvider value={childDialog}>
<DialogPopup className="sm:max-w-sm" showCloseButton={false}>
<DialogHeader>
<DialogTitle>Edit details</DialogTitle>
<DialogDescription>
Update member information below.
</DialogDescription>
</DialogHeader>
<DialogPanel className="grid gap-4">
<Input defaultValue="Bora Baloglu" aria-label="Name" />
<Input
defaultValue="bora@example.com"
aria-label="Email"
type="email"
/>
</DialogPanel>
<DialogFooter>
<DialogClose asChild>
<Button size="sm" type="button" variant="ghost">
Cancel
</Button>
</DialogClose>
<Button size="sm" type="button">
Save changes
</Button>
</DialogFooter>
</DialogPopup>
</DialogRootProvider>
</>
);
};
export default DialogNestedDemo;
Non Modal
Edit profile
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
const DialogNonModal = () => (
<Dialog modal={false}>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
Open non-modal dialog
</Button>
</DialogTrigger>
<DialogPopup className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes here. Save when you are done.
</DialogDescription>
</DialogHeader>
<DialogPanel>
<Input defaultValue="Jane Doe" aria-label="Name" />
</DialogPanel>
<DialogFooter>
<DialogClose asChild>
<Button size="sm" variant="ghost">
Cancel
</Button>
</DialogClose>
<Button size="sm" type="button">
Save
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
);
export default DialogNonModal;
Controlled
Session settings
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
const DialogControlledDemo = () => {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={(details) => setOpen(details.open)}>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
Open controlled dialog
</Button>
</DialogTrigger>
<DialogPopup className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Session settings</DialogTitle>
<DialogDescription>
This dialog is controlled with React state and Ark's
`onOpenChange`.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
size="sm"
type="button"
variant="ghost"
onClick={() => setOpen(false)}
>
Close
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
);
};
export default DialogControlledDemo;
Initial Focus
Edit profile
import { useRef } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
const DialogInitialFocusDemo = () => {
const inputRef = useRef<HTMLInputElement>(null);
return (
<Dialog initialFocusEl={() => inputRef.current}>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
Open initial focus dialog
</Button>
</DialogTrigger>
<DialogPopup className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
The first input receives focus when the dialog opens.
</DialogDescription>
</DialogHeader>
<DialogPanel className="grid gap-3">
<Input ref={inputRef} defaultValue="Jane Doe" aria-label="Name" />
<Input
defaultValue="jane@example.com"
aria-label="Email"
type="email"
/>
</DialogPanel>
<DialogFooter>
<DialogClose asChild>
<Button size="sm" type="button" variant="ghost">
Cancel
</Button>
</DialogClose>
<Button size="sm" type="button">
Save
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
);
};
export default DialogInitialFocusDemo;
Multiple Triggers
Edit user
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
type User = {
email: string;
id: string;
name: string;
};
const users: User[] = [
{ id: "1", name: "Alice Johnson", email: "alice@example.com" },
{ id: "2", name: "Bob Smith", email: "bob@example.com" },
{ id: "3", name: "Carol Davis", email: "carol@example.com" },
];
const DialogMultipleTriggersDemo = () => {
const [activeUser, setActiveUser] = useState<User | null>(null);
return (
<Dialog
onTriggerValueChange={(details) => {
setActiveUser(users.find((user) => user.id === details.value) ?? null);
}}
>
<div className="flex flex-wrap gap-2">
{users.map((user) => (
<DialogTrigger asChild key={user.id} value={user.id}>
<Button size="sm" variant="outline">
Edit {user.name}
</Button>
</DialogTrigger>
))}
</div>
<DialogPopup className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Edit user</DialogTitle>
<DialogDescription>
Multiple triggers open one dialog and pass the selected user through
trigger value.
</DialogDescription>
</DialogHeader>
{activeUser ? (
<>
<DialogPanel className="grid gap-3">
<Input aria-label="Name" defaultValue={activeUser.name} />
<Input
aria-label="Email"
defaultValue={activeUser.email}
type="email"
/>
</DialogPanel>
<DialogFooter>
<DialogClose asChild>
<Button size="sm" type="button" variant="ghost">
Cancel
</Button>
</DialogClose>
<Button size="sm" type="button">
Save
</Button>
</DialogFooter>
</>
) : null}
</DialogPopup>
</Dialog>
);
};
export default DialogMultipleTriggersDemo;
Inside Scroll
Terms of service
1. Acceptance of terms
By using this service, you agree to be bound by these terms and conditions.
2. Use license
Permission is granted to use this service for personal and internal business use.
3. User responsibilities
You are responsible for keeping your account credentials confidential.
4. Privacy policy
Your use of this service is subject to our privacy and data retention policy.
5. Revisions
We may update these terms at any time. Continued use means acceptance.
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
const sections = [
{
body: "By using this service, you agree to be bound by these terms and conditions.",
title: "1. Acceptance of terms",
},
{
body: "Permission is granted to use this service for personal and internal business use.",
title: "2. Use license",
},
{
body: "You are responsible for keeping your account credentials confidential.",
title: "3. User responsibilities",
},
{
body: "Your use of this service is subject to our privacy and data retention policy.",
title: "4. Privacy policy",
},
{
body: "We may update these terms at any time. Continued use means acceptance.",
title: "5. Revisions",
},
];
const DialogInsideScrollDemo = () => (
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
Open inside scroll dialog
</Button>
</DialogTrigger>
<DialogPopup className="sm:max-w-md" showCloseButton={false}>
<DialogHeader>
<DialogTitle>Terms of service</DialogTitle>
<DialogDescription>
This follows Ark UI's inside-scroll dialog pattern.
</DialogDescription>
</DialogHeader>
<DialogPanel className="max-h-72 overflow-y-auto pr-2">
<div className="grid gap-4">
{sections.map((section) => (
<section className="grid gap-1" key={section.title}>
<h4 className="font-medium text-sm">{section.title}</h4>
<p className="text-muted-foreground text-sm">{section.body}</p>
</section>
))}
</div>
</DialogPanel>
<DialogFooter>
<DialogClose asChild>
<Button size="sm" type="button" variant="ghost">
Decline
</Button>
</DialogClose>
<DialogClose asChild>
<Button size="sm" type="button">
Accept
</Button>
</DialogClose>
</DialogFooter>
</DialogPopup>
</Dialog>
);
export default DialogInsideScrollDemo;
Outside Scroll
Privacy Policy
1. Information We Collect
We collect information you provide directly, such as when you create an account, make a purchase, or contact us for support. This may include your name, email address, and payment information.
2. How We Use Your Information
We use the information we collect to provide and improve our services, process transactions, send communications, and personalize your experience.
3. Information Sharing
We do not sell your personal information. We may share information with service providers who assist in our operations, or when required by law.
4. Data Security
We implement appropriate technical and organizational measures to protect your personal information against unauthorized access, alteration, or destruction.
5. Your Rights
You have the right to access, correct, or delete your personal information. You may also opt out of marketing communications at any time.
6. Cookies and Tracking
We use cookies and similar technologies to enhance your experience, analyze usage patterns, and deliver targeted content. You can manage cookie preferences in your browser settings.
7. Third-Party Services
Our service may contain links to third-party websites. We are not responsible for the privacy practices of these external sites.
8. Children Privacy
Our services are not directed to children under 13. We do not knowingly collect personal information from children without parental consent.
9. International Transfers
Your information may be transferred to and processed in countries other than your own. We ensure appropriate safeguards are in place for such transfers.
10. Changes to This Policy
We may update this privacy policy from time to time. We will notify you of significant changes by posting a notice on our website or sending you an email.
11. Data Retention
We retain your personal information for as long as necessary to fulfill the purposes outlined in this policy, unless a longer retention period is required by law.
12. Contact Us
If you have questions about this privacy policy or our data practices, please contact our privacy team through the support channels provided on our website.
13. Long Section
This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.
14. Long Section
This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.
15. Long Section
This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.
16. Long Section
This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.
17. Long Section
This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.
18. Long Section
This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.
19. Long Section
This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.
20. Long Section
This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.
import { useRef } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogDescription,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
const CONTENT_SECTIONS = [
{
body: "We collect information you provide directly, such as when you create an account, make a purchase, or contact us for support. This may include your name, email address, and payment information.",
title: "1. Information We Collect",
},
{
body: "We use the information we collect to provide and improve our services, process transactions, send communications, and personalize your experience.",
title: "2. How We Use Your Information",
},
{
body: "We do not sell your personal information. We may share information with service providers who assist in our operations, or when required by law.",
title: "3. Information Sharing",
},
{
body: "We implement appropriate technical and organizational measures to protect your personal information against unauthorized access, alteration, or destruction.",
title: "4. Data Security",
},
{
body: "You have the right to access, correct, or delete your personal information. You may also opt out of marketing communications at any time.",
title: "5. Your Rights",
},
{
body: "We use cookies and similar technologies to enhance your experience, analyze usage patterns, and deliver targeted content. You can manage cookie preferences in your browser settings.",
title: "6. Cookies and Tracking",
},
{
body: "Our service may contain links to third-party websites. We are not responsible for the privacy practices of these external sites.",
title: "7. Third-Party Services",
},
{
body: "Our services are not directed to children under 13. We do not knowingly collect personal information from children without parental consent.",
title: "8. Children Privacy",
},
{
body: "Your information may be transferred to and processed in countries other than your own. We ensure appropriate safeguards are in place for such transfers.",
title: "9. International Transfers",
},
{
body: "We may update this privacy policy from time to time. We will notify you of significant changes by posting a notice on our website or sending you an email.",
title: "10. Changes to This Policy",
},
{
body: "We retain your personal information for as long as necessary to fulfill the purposes outlined in this policy, unless a longer retention period is required by law.",
title: "11. Data Retention",
},
{
body: "If you have questions about this privacy policy or our data practices, please contact our privacy team through the support channels provided on our website.",
title: "12. Contact Us",
},
{
body: "This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.",
title: "13. Long Section",
},
{
body: "This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.",
title: "14. Long Section",
},
{
body: "This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.",
title: "15. Long Section",
},
{
body: "This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.",
title: "16. Long Section",
},
{
body: "This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.",
title: "17. Long Section",
},
{
body: "This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.",
title: "18. Long Section",
},
{
body: "This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.",
title: "19. Long Section",
},
{
body: "This is a long section of text to test the outside scroll dialog. It should be long enough to cause the dialog to scroll outside the viewport.",
title: "20. Long Section",
},
];
const DialogOutsideScrollDemo = () => {
const contentRef = useRef<HTMLDivElement | null>(null);
return (
<Dialog initialFocusEl={() => contentRef.current}>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
Open outside scroll dialog
</Button>
</DialogTrigger>
<DialogPopup
bottomStickOnMobile={false}
className="sm:max-w-md my-16 max-h-none"
id="dialog-outside-scroll-content"
positionerClassName="overflow-y-auto items-start py-8"
>
<DialogHeader>
<DialogTitle>Privacy Policy</DialogTitle>
<DialogDescription>
This layout allows the dialog to extend beyond the viewport while
keeping the outer container scrollable.
</DialogDescription>
</DialogHeader>
<DialogPanel
className="grid gap-4"
ref={(node: HTMLDivElement | null) => {
contentRef.current = node;
}}
>
{CONTENT_SECTIONS.map((section) => (
<section className="grid gap-1" key={section.title}>
<h4 className="font-medium text-sm">{section.title}</h4>
<p className="text-muted-foreground text-sm">{section.body}</p>
</section>
))}
</DialogPanel>
</DialogPopup>
</Dialog>
);
};
export default DialogOutsideScrollDemo;
Confirmation
Edit content
Unsaved changes
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogRootProvider,
DialogTitle,
useDialog,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
const DialogConfirmationDemo = () => {
const [formContent, setFormContent] = useState("");
const confirmDialog = useDialog();
const parentDialog = useDialog({
onOpenChange: (details) => {
if (!details.open && formContent.trim()) {
confirmDialog.setOpen(true);
return;
}
confirmDialog.setOpen(false);
if (!details.open) {
setFormContent("");
}
},
});
const handleDiscardChanges = () => {
setFormContent("");
confirmDialog.setOpen(false);
parentDialog.setOpen(false);
};
return (
<>
<Button
size="sm"
type="button"
variant="outline"
onClick={() => parentDialog.setOpen(true)}
>
Open confirmation dialog
</Button>
<DialogRootProvider value={parentDialog}>
<DialogPopup className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Edit content</DialogTitle>
<DialogDescription>
Try typing and then closing. A confirmation dialog appears before
discarding changes.
</DialogDescription>
</DialogHeader>
<DialogPanel>
<Textarea
value={formContent}
onChange={(event) => setFormContent(event.target.value)}
placeholder="Type something here..."
rows={4}
/>
</DialogPanel>
<DialogFooter>
<DialogClose asChild>
<Button size="sm" type="button" variant="ghost">
Close
</Button>
</DialogClose>
</DialogFooter>
</DialogPopup>
</DialogRootProvider>
<DialogRootProvider value={confirmDialog}>
<DialogPopup className="sm:max-w-sm" showCloseButton={false}>
<DialogHeader>
<DialogTitle>Unsaved changes</DialogTitle>
<DialogDescription>
You have unsaved changes. Do you want to discard them?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
size="sm"
type="button"
variant="ghost"
onClick={() => confirmDialog.setOpen(false)}
>
Keep editing
</Button>
<Button size="sm" type="button" onClick={handleDiscardChanges}>
Discard changes
</Button>
</DialogFooter>
</DialogPopup>
</DialogRootProvider>
</>
);
};
export default DialogConfirmationDemo;
API reference
This component mirrors the upstream Ark UI primitive. All props and DOM behavior are defined by Ark unless you see an ark-cn-only row below.
DialogPopup
| Prop | Type | Description |
|---|---|---|
| backdropClassName? | string | Extra class for the backdrop element. |
| bottomStickOnMobile? | boolean | Bottom-aligned positioner on small screens instead of centered. |
| closeProps? | CloseTriggerProps | Props forwarded to the built-in close button. |
| positionerClassName? | string | Extra class on the positioner wrapper. |
| showCloseButton? | boolean | Toggles the default close button. |
| size? | "default" | "sm" | Preset max-width for the content shell. |
DialogFooter
| Prop | Type | Description |
|---|---|---|
| variant? | "default" | "bare" | Bordered/muted footer bar vs minimal padding only. |
DialogPanel
| Prop | Type | Description |
|---|---|---|
| scrollFade? | boolean | Applies a vertical fade mask on scroll overflow. |
| scrollable? | boolean | Enables vertical scrolling with stable scrollbar gutter. |
See the ARK UI documentation for the full API.
Accessibility
See the Ark UI documentation for clarification.