Dialog

free

Dialog 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 Dialog

Examples

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.

PropDescriptionTypeDefault
childrenModal content rendered inside the dialog surface.ReactNode-
classNameClass names applied to the dialog content surface.string-
closableShows the default close button in the top corner.booleantrue
heightFixed height applied to the dialog content surface.string | number-
isOpenControls whether the dialog is visible.booleanfalse
lockScrollLocks document scrolling through the overlay while the dialog is mounted.booleanfalse
onCloseCalled when the dialog asks to close through the close button, overlay, or Escape key. Update isOpen in this handler.DialogOpenChangeHandler-
onOpenCalled when the floating layer reports that the dialog opened.DialogOpenChangeHandler-
overlayClassNameClass names applied to the overlay element.string-
refRef forwarded to the dialog content element.Ref<HTMLDivElement>-
shouldCloseOnEscAllows Escape to request closing the dialog.booleantrue
shouldCloseOnOverlayClickAllows an overlay click to request closing the dialog.booleantrue
widthWidth of the dialog in pixels. Falls back to auto width when the viewport is narrower.number520
...nativeDivPropsNative props applied to the dialog content element.NativeDialogDivProps-

Types

type DialogOpenChangeHandler = (open: boolean) => void;
type NativeDialogDivProps = ComponentProps<'div'>;