Dialog
freeDialog creates a focused modal step for confirmations and short tasks.
@floating-ui/react
Preview
Dark
import { useState } from 'react';
import { PiArrowsLeftRight, PiEye, PiEyeSlash } from 'react-icons/pi'
import Button from '@/components/ui/Button';
import Dialog from '@/components/ui/Dialog';
import Form from '@/components/ui/Form';
import Input from '@/components/ui/Input';
export default function UsageDemo() {
const [open, setOpen] = useState(false);
const [visible, setVisible] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Modal
</Button>
<Dialog isOpen={open} width={420} onClose={() => setOpen(false)}>
<div className="flex flex-col items-center pt-4 text-center">
<div className="flex items-center gap-2 mt-4">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl border bg-card shadow-md">
<img
src="/img/thumbs/brands/notion.png"
alt="Notion"
className="h-8 w-8 object-contain"
/>
</div>
<PiArrowsLeftRight className="h-5 w-5 shrink-0 text-muted-foreground" />
<div className="flex h-16 w-16 items-center justify-center rounded-2xl border bg-card shadow-md">
<img
src="/img/thumbs/brands/google.png"
alt="Google"
className="h-8 w-8 object-contain"
/>
</div>
</div>
<h5 className="mt-8 text-xl font-semibold text-foreground">
Connect your calendar
</h5>
<p className="mt-2 text-muted-foreground">
Meetings from your calendar provider will sync here automatically
once you sign in below.
</p>
</div>
<Form
className="mt-8 mb-4 px-2"
onSubmit={(event) => {
event.preventDefault();
setOpen(false);
}}
>
<Form.Field label="Email" htmlFor="dialog-usage-email">
<Input
id="dialog-usage-email"
type="email"
placeholder="you@company.com"
/>
</Form.Field>
<Form.Field label="Password" asterisk htmlFor="dialog-usage-password">
<Input
id="dialog-usage-password"
type={visible ? 'text' : 'password'}
suffix={
<button
type="button"
aria-label={visible ? 'Hide password' : 'Show password'}
className="inline-flex cursor-pointer items-center text-muted-foreground transition-colors hover:text-foreground"
onClick={() => setVisible((current) => !current)}
>
{visible ? (
<PiEyeSlash className="text-base" />
) : (
<PiEye className="text-base" />
)}
</button>
}
/>
</Form.Field>
<Button type="submit" block variant="solid">
Continue
</Button>
</Form>
</Dialog>
</div>
);
}Installation
Add this component with the NateUI CLI.
npx nateui@latest add DialogExamples
Basic
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Dialog from '@/components/ui/Dialog';
import { PiLightning } from 'react-icons/pi'
export default function BasicDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Modal
</Button>
<Dialog isOpen={open} onClose={() => setOpen(false)}>
<span className="flex h-10 w-10 items-center justify-center rounded-control border text-foreground">
<PiLightning className="text-lg text-primary" />
</span>
<h5 className="mt-4 text-lg font-semibold text-foreground">
Enable instant sync
</h5>
<p className="mt-1 text-muted-foreground">
Every change saves and syncs across devices immediately, without
waiting for a manual refresh.
</p>
<div className="mt-4 flex gap-2 justify-end">
<Button onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
variant="solid"
onClick={() => setOpen(false)}
>
Enable sync
</Button>
</div>
</Dialog>
</div>
);
}Size
Preview
Dark
import { useState } from 'react';
import {
PiBookmarkSimple,
PiCode,
PiCrown,
PiHeadset,
PiProhibit,
PiSparkle,
} from 'react-icons/pi'
import Button from '@/components/ui/Button';
import Dialog from '@/components/ui/Dialog';
const features = [
{ icon: PiHeadset, label: 'Priority support' },
{ icon: PiBookmarkSimple, label: 'Save unlimited projects' },
{ icon: PiCode, label: 'Export production-ready code' },
{ icon: PiProhibit, label: 'Ad-free experience' },
{
icon: PiSparkle,
label: 'Access to premium illustrations',
description:
'Premium illustrations unlock advanced patterns not available on the free tier.',
},
];
export default function SizeDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Modal
</Button>
<Dialog
isOpen={open}
width={780}
className="overflow-hidden p-0"
onClose={() => setOpen(false)}
>
<div className="flex h-full">
<div className="flex w-full max-w-100 shrink-0 flex-col p-8">
<span className="flex h-10 w-10 items-center justify-center rounded-control border text-foreground">
<PiCrown className="text-lg" />
</span>
<h5 className="mt-4 text-xl font-semibold text-foreground">
Upgrade to Pro
</h5>
<p className="mt-1 text-muted-foreground">
Get access to every benefit below with one upgrade.
</p>
<div className="mt-4 flex items-baseline gap-1">
<span className="text-3xl font-semibold text-foreground">
$79
</span>
<span className="text-muted-foreground">monthly</span>
</div>
<ul className="mt-8 space-y-2">
{features.map((feature) => (
<li key={feature.label} className="flex items-start gap-2">
<feature.icon className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
<div>
<p className="text-foreground">{feature.label}</p>
{feature.description && (
<p className="mt-1 text-xs text-muted-foreground">
{feature.description}
</p>
)}
</div>
</li>
))}
</ul>
<Button
block
variant="solid"
className="mt-8"
onClick={() => setOpen(false)}
>
Unlock every feature
</Button>
</div>
<div className="relative hidden flex-1 items-center justify-center overflow-hidden bg-muted sm:flex">
<div className="absolute top-0 left-0 w-full h-full ">
<svg
width="100%"
height="100%"
className="h-full w-full text-gray-900/20 dark:text-gray-100/20"
>
<defs>
<pattern id="slash" width={10} height={10} patternUnits="userSpaceOnUse">
<rect x="5.5" y="5.5" width={1} height={1} fill="currentColor" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#slash)" />
</svg>
</div>
<img
src="/img/thumbs/misc/img-2.png"
alt=""
className="absolute h-50 w-42 -translate-x-16 translate-y-6 -rotate-6 rounded-lg border-4 border-card object-cover shadow-lg"
/>
<img
src="/img/thumbs/misc/img-4.png"
alt=""
className="absolute h-54 w-46 translate-x-10 rotate-3 rounded-lg border-4 border-card object-cover shadow-xl"
/>
<img
src="/img/thumbs/misc/img-6.png"
alt=""
className="absolute h-50 w-42 -translate-x-4 -translate-y-20 -rotate-3 rounded-lg border-4 border-card object-cover shadow-lg"
/>
</div>
</div>
</Dialog>
</div>
);
}Custom Backdrop
Preview
Dark
import { useState } from 'react';
import { PiLightning } from 'react-icons/pi'
import Button from '@/components/ui/Button';
import Dialog from '@/components/ui/Dialog';
export default function OverlayClassNameDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Modal
</Button>
<Dialog
isOpen={open}
overlayClassName="bg-black/50 backdrop-blur-none"
onClose={() => setOpen(false)}
>
<span className="flex h-10 w-10 items-center justify-center rounded-control border text-foreground">
<PiLightning className="text-lg text-primary" />
</span>
<h5 className="mt-4 text-lg font-semibold text-foreground">
Enable instant sync
</h5>
<p className="mt-1 text-muted-foreground">
Every change saves and syncs across devices immediately, without
waiting for a manual refresh.
</p>
<div className="mt-4 flex gap-2 justify-end">
<Button onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
variant="solid"
onClick={() => setOpen(false)}
>
Enable sync
</Button>
</div>
</Dialog>
</div>
);
}Internal Scroll
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Dialog from '@/components/ui/Dialog';
import Scroll from '@/components/ui/Scroll';
const sections = [
{
title: 'Acceptance of terms',
body: "Creating an account means you're agreeing to everything laid out on this page. Stop using the service if any part of it doesn't work for you.",
},
{
title: 'Account responsibilities',
body: 'Keep your password somewhere safe — anything that happens while someone is signed in as you counts as your activity. Spot something unusual? Reach out to support before it becomes a bigger problem.',
},
{
title: 'Data & privacy',
body: "We collect only what's needed to run the service and never sell it to third parties. Full details live in our separate privacy policy.",
},
{
title: 'Changes to these terms',
body: 'Terms get updated occasionally as the product changes. Keep using the service afterward and that counts as accepting the new version.',
},
{
title: 'Termination',
body: "We can suspend or close an account that breaks these terms. You're welcome to stop using the service at any time — just email us and we'll wrap things up.",
},
{
title: 'Limitation of liability',
body: "The service is provided as-is. We're not liable for indirect losses like lost profits or missed opportunities that come from using it.",
},
{
title: 'Governing law',
body: "These terms follow the laws of the region where the company is registered, regardless of where you're using the service from.",
},
{
title: 'Getting in touch',
body: "Questions about any of this? Reach out through the support link in your account settings and we'll get back to you within a couple of business days.",
},
];
export default function InternalScrollDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Modal
</Button>
<Dialog isOpen={open} onClose={() => setOpen(false)}>
<div className="pb-4">
<h5 className="text-lg font-semibold text-foreground">
Terms & Conditions
</h5>
<p className="mt-1 text-muted-foreground">
Review each section below, then confirm you agree to continue.
</p>
</div>
<Scroll className="h-120" scrollbars="vertical">
<div className="space-y-4 py-4 pe-2">
{sections.map((section, index) => (
<div key={section.title}>
<div className="text-medium">
{index + 1}. {section.title}
</div>
<p className="mt-1 text-muted-foreground">{section.body}</p>
</div>
))}
</div>
</Scroll>
<div className="flex justify-end gap-2 border-t pt-4">
<Button onClick={() => setOpen(false)}>Decline</Button>
<Button variant="solid" onClick={() => setOpen(false)}>
Accept
</Button>
</div>
</Dialog>
</div>
);
}Custom Layout
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Dialog from '@/components/ui/Dialog';
import Form from '@/components/ui/Form';
import Input from '@/components/ui/Input';
export default function CustomLayoutDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Modal
</Button>
<Dialog
className="overflow-hidden p-0"
isOpen={open}
onClose={() => setOpen(false)}
>
<Form
onSubmit={(event) => {
event.preventDefault();
setOpen(false);
}}
>
<div className="border-b px-4 py-4">
<h5 className="text-lg font-semibold text-foreground">
Edit details
</h5>
<p className="mt-1 text-muted-foreground">
Use the Form component when the dialog contains editable fields.
</p>
</div>
<div className="p-4">
<Form.Field label="Field one" htmlFor="dialog-field-one">
<Input
id="dialog-field-one"
defaultValue="Example value"
/>
</Form.Field>
<Form.Field label="Field two" htmlFor="dialog-field-two">
<Input
id="dialog-field-two"
defaultValue="Supporting detail"
/>
</Form.Field>
</div>
<div className="flex justify-end gap-2 border-t bg-muted px-4 py-3">
<Button type="button" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button type="submit" variant="solid">
Confirm
</Button>
</div>
</Form>
</Dialog>
</div>
);
}Closable
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Dialog from '@/components/ui/Dialog';
import { PiLightning } from 'react-icons/pi'
export default function ClosableDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Modal
</Button>
<Dialog closable={false} isOpen={open} onClose={() => setOpen(false)}>
<span className="flex h-10 w-10 items-center justify-center rounded-control border text-foreground">
<PiLightning className="text-lg text-primary" />
</span>
<h5 className="mt-4 text-lg font-semibold text-foreground">
Enable instant sync
</h5>
<p className="mt-1 text-muted-foreground">
Every change saves and syncs across devices immediately, without
waiting for a manual refresh.
</p>
<div className="mt-4 flex gap-2 justify-end">
<Button onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
variant="solid"
onClick={() => setOpen(false)}
>
Enable sync
</Button>
</div>
</Dialog>
</div>
);
}Lock Scroll
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Dialog from '@/components/ui/Dialog';
import { PiLightning } from 'react-icons/pi'
export default function LockScrollDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Modal
</Button>
<Dialog lockScroll isOpen={open} onClose={() => setOpen(false)}>
<span className="flex h-10 w-10 items-center justify-center rounded-control border text-foreground">
<PiLightning className="text-lg text-primary" />
</span>
<h5 className="mt-4 text-lg font-semibold text-foreground">
Enable instant sync
</h5>
<p className="mt-1 text-muted-foreground">
Every change saves and syncs across devices immediately, without
waiting for a manual refresh.
</p>
<div className="mt-4 flex gap-2 justify-end">
<Button onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
variant="solid"
onClick={() => setOpen(false)}
>
Enable sync
</Button>
</div>
</Dialog>
</div>
);
}API
Dialog accepts the props below plus native props for the dialog content div.
| Prop | Description | Type | Default |
|---|---|---|---|
children | Modal content rendered inside the dialog surface. | ReactNode | - |
className | Class names applied to the dialog content surface. | string | - |
closable | Shows the default close button in the top corner. | boolean | true |
height | Fixed height applied to the dialog content surface. | string | number | - |
isOpen | Controls whether the dialog is visible. | boolean | false |
lockScroll | Locks document scrolling through the overlay while the dialog is mounted. | boolean | false |
onClose | Called when the dialog asks to close through the close button, overlay, or Escape key. Update isOpen in this handler. | DialogOpenChangeHandler | - |
onOpen | Called when the floating layer reports that the dialog opened. | DialogOpenChangeHandler | - |
overlayClassName | Class names applied to the overlay element. | string | - |
ref | Ref forwarded to the dialog content element. | Ref<HTMLDivElement> | - |
shouldCloseOnEsc | Allows Escape to request closing the dialog. | boolean | true |
shouldCloseOnOverlayClick | Allows an overlay click to request closing the dialog. | boolean | true |
width | Width of the dialog in pixels. Falls back to auto width when the viewport is narrower. | number | 520 |
...nativeDivProps | Native props applied to the dialog content element. | NativeDialogDivProps | - |
Types
type DialogOpenChangeHandler = (open: boolean) => void;
type NativeDialogDivProps = ComponentProps<'div'>;