---
title: Dialog
description: Modal panel for forms, details, and confirmations.
group: Components
order: 32
---

Open a focused panel over the page for forms, details, or confirmations. Dismiss it with Escape or the close control.

```tsx
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Field, FieldGroup } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function Example() {
  return (
    <Dialog>
      <form>
        <DialogTrigger render={<Button variant="outline" />}>
          Open Dialog
        </DialogTrigger>
        <DialogContent className="sm:max-w-sm">
          <DialogHeader>
            <DialogTitle>Edit profile</DialogTitle>
            <DialogDescription>
              Make changes to your profile here. Click save when you&apos;re
              done.
            </DialogDescription>
          </DialogHeader>
          <FieldGroup>
            <Field>
              <Label htmlFor="name-1">Name</Label>
              <Input id="name-1" name="name" defaultValue="Pedro Duarte" />
            </Field>
            <Field>
              <Label htmlFor="username-1">Username</Label>
              <Input
                id="username-1"
                name="username"
                defaultValue="@peduarte"
              />
            </Field>
          </FieldGroup>
          <DialogFooter>
            <DialogClose render={<Button variant="outline" />}>
              Cancel
            </DialogClose>
            <Button type="submit">Save changes</Button>
          </DialogFooter>
        </DialogContent>
      </form>
    </Dialog>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add dialog
```

## Usage

```tsx
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
```

```tsx
<Dialog>
  <DialogTrigger render={<Button variant="outline" />}>
    Edit profile
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Edit profile</DialogTitle>
      <DialogDescription>
        Update your name and email for this workspace.
      </DialogDescription>
    </DialogHeader>
  </DialogContent>
</Dialog>
```

## Composition

```tree
Dialog
├── DialogTrigger
└── DialogContent
    ├── DialogHeader
    │   ├── DialogTitle
    │   └── DialogDescription
    └── DialogFooter
```

## Custom close button

Replace the default close control with a button in the footer.

```tsx
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function Example() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>
        Share
      </DialogTrigger>
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle>Share link</DialogTitle>
          <DialogDescription>
            Anyone who has this link will be able to view this.
          </DialogDescription>
        </DialogHeader>
        <div className="flex items-center gap-2">
          <div className="grid flex-1 gap-2">
            <Label htmlFor="link" className="sr-only">
              Link
            </Label>
            <Input
              id="link"
              defaultValue="https://example.com/share/a1b2c3d4"
              readOnly
            />
          </div>
        </div>
        <DialogFooter className="sm:justify-start">
          <DialogClose render={<Button />}>Close</DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
```

## No close button

Set `showCloseButton={false}` on `DialogContent` to hide the default close button.

```tsx
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

export function Example() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>
        No Close Button
      </DialogTrigger>
      <DialogContent showCloseButton={false}>
        <DialogHeader>
          <DialogTitle>No Close Button</DialogTitle>
          <DialogDescription>
            This dialog doesn&apos;t have a close button in the top-right
            corner.
          </DialogDescription>
        </DialogHeader>
      </DialogContent>
    </Dialog>
  )
}
```

## Sticky footer

Keep actions in view while longer content scrolls.

```tsx
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

export function Example() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>
        Sticky Footer
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Sticky Footer</DialogTitle>
          <DialogDescription>
            This dialog has a sticky footer that stays visible while the content
            scrolls.
          </DialogDescription>
        </DialogHeader>
        <div className="-mx-4 max-h-[50vh] overflow-y-auto px-4">
          {Array.from({ length: 10 }).map((_, index) => (
            <p key={index} className="mb-4 leading-normal tracking-wide">
              Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
              eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
              enim ad minim veniam, quis nostrud exercitation ullamco laboris
              nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
              reprehenderit in voluptate velit esse cillum dolore eu fugiat
              nulla pariatur. Excepteur sint occaecat cupidatat non proident,
              sunt in culpa qui officia deserunt mollit anim id est laborum.
            </p>
          ))}
        </div>
        <DialogFooter>
          <DialogClose render={<Button variant="outline" />}>
            Close
          </DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
```

## Scrollable content

Let longer content scroll while the header stays in view.

```tsx
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

export function Example() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>
        Scrollable Content
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Scrollable Content</DialogTitle>
          <DialogDescription>
            This is a dialog with scrollable content.
          </DialogDescription>
        </DialogHeader>
        <div className="-mx-4 max-h-[50vh] overflow-y-auto px-4">
          {Array.from({ length: 10 }).map((_, index) => (
            <p key={index} className="mb-4 leading-normal tracking-wide">
              Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
              eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
              enim ad minim veniam, quis nostrud exercitation ullamco laboris
              nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
              reprehenderit in voluptate velit esse cillum dolore eu fugiat
              nulla pariatur. Excepteur sint occaecat cupidatat non proident,
              sunt in culpa qui officia deserunt mollit anim id est laborum.
            </p>
          ))}
        </div>
      </DialogContent>
    </Dialog>
  )
}
```

## API Reference

### Dialog

| Prop | Type | Default |
| --- | --- | --- |
| open | boolean | — |
| defaultOpen | boolean | false |
| onOpenChange | (open: boolean) => void | — |
| onOpenChangeComplete | (open: boolean) => void | — |
| modal | boolean \| "trap-focus" | true |
| disablePointerDismissal | boolean | false |

```tsx
<Dialog>
  <DialogTrigger render={<Button variant="outline" />}>
    Open
  </DialogTrigger>
  <DialogContent>...</DialogContent>
</Dialog>
```

### DialogTrigger

Renders a native button by default. Pass a [Button](/docs/components/button) with the Base UI `render` prop.

| Prop | Type | Default |
| --- | --- | --- |
| className | string | — |
| render | React.ReactElement | — |

```tsx
<DialogTrigger render={<Button variant="outline" />}>
  Open
</DialogTrigger>
```

### DialogClose

Closes the dialog. Prefer `render={<Button … />}` so actions match your buttons.

| Prop | Type | Default |
| --- | --- | --- |
| className | string | — |
| render | React.ReactElement | — |

```tsx
<DialogClose render={<Button variant="outline" />}>
  Cancel
</DialogClose>
```

### DialogContent

Centered panel with an overlay and the shared Arctis open animation. A top-right close control is included by default.

| Prop | Type | Default |
| --- | --- | --- |
| className | string | — |
| showCloseButton | boolean | true |

```tsx
<DialogContent className="sm:max-w-md" showCloseButton>
  ...
</DialogContent>
```

### DialogHeader

| Prop | Type | Default |
| --- | --- | --- |
| className | string | — |

```tsx
<DialogHeader>
  <DialogTitle>Title</DialogTitle>
  <DialogDescription>Description</DialogDescription>
</DialogHeader>
```

### DialogFooter

| Prop | Type | Default |
| --- | --- | --- |
| className | string | — |
| showCloseButton | boolean | false |

```tsx
<DialogFooter>
  <DialogClose render={<Button variant="outline" />}>
    Cancel
  </DialogClose>
  <Button>Save</Button>
</DialogFooter>
```

### DialogTitle

| Prop | Type | Default |
| --- | --- | --- |
| className | string | — |

```tsx
<DialogTitle>Title</DialogTitle>
```

### DialogDescription

| Prop | Type | Default |
| --- | --- | --- |
| className | string | — |

```tsx
<DialogDescription>Description</DialogDescription>
```

### DialogPortal

| Prop | Type | Default |
| --- | --- | --- |
| container | HTMLElement | document.body |

### DialogOverlay

Scrim behind the panel. Rendered automatically by `DialogContent`.

| Prop | Type | Default |
| --- | --- | --- |
| className | string | — |

## Source

```tsx
"use client";

import {
  createContext,
  useContext,
  useState,
  type ComponentProps,
} from "react";
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { useOverlayEntered } from "@/lib/use-overlay-entered";

function mergeClass<State>(
  base: string,
  className?: string | ((state: State) => string | undefined),
) {
  if (typeof className === "function") {
    return (state: State) => cn(base, className(state));
  }
  return cn(base, className);
}

type DialogContextValue = {
  open: boolean;
  entered: boolean;
};

const DialogContext = createContext<DialogContextValue | null>(null);

function useDialog() {
  const context = useContext(DialogContext);
  if (!context) {
    throw new Error("Dialog parts must be used within <Dialog>");
  }
  return context;
}

function Dialog({
  open: openProp,
  defaultOpen = false,
  onOpenChange,
  ...props
}: DialogPrimitive.Root.Props) {
  const [uncontrolled, setUncontrolled] = useState(defaultOpen);
  const controlled = openProp !== undefined;
  const open = controlled ? Boolean(openProp) : uncontrolled;
  const entered = useOverlayEntered(open);

  return (
    <DialogContext.Provider value={{ open, entered }}>
      <DialogPrimitive.Root
        data-slot="dialog"
        open={open}
        onOpenChange={(next, eventDetails) => {
          if (!controlled) setUncontrolled(next);
          onOpenChange?.(next, eventDetails);
        }}
        {...props}
      />
    </DialogContext.Provider>
  );
}

function DialogTrigger({
  className,
  ...props
}: DialogPrimitive.Trigger.Props) {
  return (
    <DialogPrimitive.Trigger
      data-slot="dialog-trigger"
      className={mergeClass("", className)}
      {...props}
    />
  );
}

function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
  return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}

function DialogClose({ className, ...props }: DialogPrimitive.Close.Props) {
  return (
    <DialogPrimitive.Close
      data-slot="dialog-close"
      className={mergeClass("", className)}
      {...props}
    />
  );
}

function DialogOverlay({
  className,
  ...props
}: DialogPrimitive.Backdrop.Props) {
  const { entered } = useDialog();

  return (
    <DialogPrimitive.Backdrop
      data-slot="dialog-overlay"
      className={mergeClass(
        cn(
          "arctis-overlay-backdrop absolute inset-0 bg-black/40",
          entered && "arctis-overlay-backdrop-open",
        ),
        className,
      )}
      {...props}
    />
  );
}

function DialogContent({
  className,
  children,
  showCloseButton = true,
  ...props
}: DialogPrimitive.Popup.Props & {
  showCloseButton?: boolean;
}) {
  const { entered } = useDialog();

  return (
    <DialogPortal>
      <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
        <DialogOverlay />
        <DialogPrimitive.Popup
          data-slot="dialog-content"
          className={mergeClass(
            cn(
              "arctis-overlay relative z-10 grid w-full max-w-[calc(100%-2rem)] origin-center gap-4 rounded-md border border-border bg-card p-5 text-sm tracking-wide text-card-foreground outline-none sm:max-w-sm",
              showCloseButton && "[&_[data-slot=dialog-header]]:pr-10",
              entered && "arctis-overlay-open",
            ),
            className,
          )}
          {...props}
        >
          {children}
          {showCloseButton ? (
            <DialogPrimitive.Close
              data-slot="dialog-close"
              className={buttonVariants({
                variant: "ghost",
                size: "icon-sm",
                className:
                  "absolute top-3 right-3 text-muted-foreground hover:text-foreground",
              })}
            >
              <XIcon className="size-4" />
              <span className="sr-only">Close</span>
            </DialogPrimitive.Close>
          ) : null}
        </DialogPrimitive.Popup>
      </div>
    </DialogPortal>
  );
}

function DialogHeader({ className, ...props }: ComponentProps<"div">) {
  return (
    <div
      data-slot="dialog-header"
      className={cn("flex flex-col gap-1.5", className)}
      {...props}
    />
  );
}

function DialogFooter({
  className,
  showCloseButton = false,
  children,
  ...props
}: ComponentProps<"div"> & {
  showCloseButton?: boolean;
}) {
  return (
    <div
      data-slot="dialog-footer"
      className={cn(
        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
        className,
      )}
      {...props}
    >
      {children}
      {showCloseButton ? (
        <DialogPrimitive.Close
          data-slot="dialog-close"
          className={buttonVariants({ variant: "outline" })}
        >
          Close
        </DialogPrimitive.Close>
      ) : null}
    </div>
  );
}

function DialogTitle({
  className,
  ...props
}: DialogPrimitive.Title.Props) {
  return (
    <DialogPrimitive.Title
      data-slot="dialog-title"
      className={mergeClass(
        "text-base font-medium tracking-wide text-foreground",
        className,
      )}
      {...props}
    />
  );
}

function DialogDescription({
  className,
  ...props
}: DialogPrimitive.Description.Props) {
  return (
    <DialogPrimitive.Description
      data-slot="dialog-description"
      className={mergeClass(
        "text-sm tracking-wide text-muted-foreground",
        className,
      )}
      {...props}
    />
  );
}

function XIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="M18 6 6 18" />
      <path d="m6 6 12 12" />
    </svg>
  );
}

export {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogOverlay,
  DialogPortal,
  DialogTitle,
  DialogTrigger,
};
```
