# Dialog

An accessible dialog with a production-friendly panel API and smooth Motion transitions.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. An agent skill is available at [/.well-known/agent-skills/site-skill.md](/.well-known/agent-skills/site-skill.md).





## Playground [#playground]

```tsx
"use client";

import { Dialog, DialogBody, DialogFooter, useDialog } from "@/components/ui/dialog";

function DoneButton() {
  const { close } = useDialog();
  return <button type="button" onClick={close}>Done</button>;
}

export function DialogDemo() {
  return (
    <>
      <Dialog
        title={"Project settings"}
        description={"Manage the details and preferences for this project."}
        trigger={<button type="button">Open dialog</button>}
        modal={true}
        contentProps={{ size: "md", showCloseButton: true, keepMounted: false }}
      >

          <>
            <DialogBody>
              Your settings form
              
            </DialogBody>
            <DialogFooter>
              <DoneButton />
            </DialogFooter>
          </>

      </Dialog>
    </>
  );
}
```

## Installation [#installation]

Initialize shadcn in your React project, then install this component from the Vandor UI registry:

```bash
npx shadcn@latest add https://vandor-ui.vercel.app/r/dialog.json
```

## Dependencies [#dependencies]

Installed automatically by the registry CLI. For manual setup:

```bash
npm install @base-ui/react@^1.8.0 cn lucide-react motion@^12.38.0
```

## Usage [#usage]

```tsx
import { Dialog, DialogBody } from "@/components/ui/dialog";

export function ProjectDetails() {
  return (
    <Dialog
      title="Project details"
      description="Manage your project preferences."
      trigger={<button type="button">View project</button>}
    >
      <DialogBody>Your project details</DialogBody>
    </Dialog>
  );
}
```

Plain children are the default. The children render function is optional, not required.
`Dialog` owns the associated trigger, portal, backdrop, header, panel, and built-in X.
Only `Dialog`, `DialogBody`, `DialogFooter`, `useDialog`, and `useDialogControl` are public components/hooks.
The upstream compound primitives remain internal.

<ComponentPreview name="dialog-demo" title="dialog-demo.tsx">
  <DialogDemo />
</ComponentPreview>

### Consumer setup [#consumer-setup]

Use React 19, Tailwind CSS 4, and an initialized shadcn theme. The registry uses Base UI 1.8+
and Motion 12.38+. The historical `new-york` source directory does not mean this is a Radix
component: its starting point is shadcn's **Base Nova** Dialog.

The single distributed `dialog.tsx` uses `cn`, `lucide-react`, `@base-ui/react`, and `motion/react`.
It does not install Button, Loading, website audio, or additional registry components.
For manual installation, install the packages listed under Dependencies and copy the complete
source into your configured UI directory. Keep the shadcn theme tokens for background,
foreground, border, muted-foreground, accent, accent-foreground, and ring.
Motion requires no animation stylesheet or provider. The module includes `"use client"`
and can be imported from a Next.js Server Component; interactive examples belong in client components.
Review the CLI overwrite prompt if your project already has `components/ui/dialog.tsx`.
The Vandor API is intentionally different from shadcn's compound API.

## Forms and stationary actions [#forms-and-stationary-actions]

<ComponentPreview name="dialog-demo" title="dialog-demo.tsx">
  <DialogFormDemo />
</ComponentPreview>

Wrap **both** `DialogBody` and `DialogFooter` in a native form with
`className="flex min-h-0 flex-1 flex-col"`. Do not put the footer inside the body or nest forms.
Body scrolling keeps the title, actions, and X visible. Any wrapper in the flex chain must retain
the constrained height (`min-h-0`, `flex-1`, and `flex-col` where applicable).
Content is deliberately not automatically wrapped in `DialogBody`.

```tsx
<Dialog title="Edit project" trigger={<button type="button">Edit</button>}>
  {({ close }) => (
    <form
      className="flex min-h-0 flex-1 flex-col"
      onSubmit={async (event) => {
        event.preventDefault();
        const values = new FormData(event.currentTarget);
        // Application-owned: handle errors and pending state here.
        await saveProject(values);
        close();
      }}
    >
      <DialogBody>{/* Your labeled fields */}</DialogBody>
      <DialogFooter>
        <button type="button" onClick={close}>
          Cancel
        </button>
        <button type="submit">Save</button>
      </DialogFooter>
    </form>
  )}
</Dialog>
```

This fragment assumes your application supplies `saveProject` and labeled fields.
Submitting does **not** close the dialog automatically. Close only after saving succeeds;
display failures and disable repeated submissions in your application. Cancel and dismissal
do not save data. The runnable demo saves only local state.

## Controllers and external openers [#controllers-and-external-openers]

<ComponentPreview name="dialog-demo" title="dialog-demo.tsx">
  <DialogExternalDemo />
</ComponentPreview>

`useDialogControl()` creates a stable controller. Bind it to one mounted Dialog with `control`.
Its read-only reactive `isOpen` reflects the **accepted** state; `open()` and `close()` request
changes through Base UI and return void, not promises. `onOpenChangeComplete` runs after animation
completion. Actions without a mounted Dialog are ignored rather than queued.
Do not combine `control` with `open`, `defaultOpen`, or `handle`; types and runtime guards reject this.
`onOpenChange` remains available to observe or cancel requests with `details.cancel()`.

An external ordinary button is not an associated trigger. Set `contentProps.finalFocus` to
its ref so focus has an explicit return target. Custom associated trigger components must
forward their ref and native button props.

For child components, call `useDialog()` at the top level:

```tsx
function DoneButton() {
  const { close } = useDialog();
  return (
    <button type="button" onClick={close}>
      Done
    </button>
  );
}
```

This lets you keep ordinary children even when a deeply nested action needs the controller.
Do not call hooks directly inside a children or header render callback; render a component instead.

## Controlled state and dismissal guards [#controlled-state-and-dismissal-guards]

Use `defaultOpen` for uncontrolled initial state, or `open` and `onOpenChange` for application-owned
state. In controlled mode, `close()` requests closing; the dialog stays open until its parent
updates `open`. Direct parent updates animate too.

<ComponentPreview name="dialog-demo" title="dialog-demo.tsx">
  <DialogControlledDemo />
</ComponentPreview>

`onOpenChange(nextOpen, details)` includes the Base UI reason, such as `escape-key`,
`outside-press`, `close-press`, or `imperative-action`. Use `details.cancel()` to reject
opening or closing without desynchronizing controllers. Set `disablePointerDismissal`
to prevent outside presses; Escape remains available.

## Sizes, persistence, and custom headers [#sizes-persistence-and-custom-headers]

`contentProps.size` defaults to `"md"`: `sm` caps width at 24rem, `md` at 32rem,
`lg` at 42rem, and `xl` at 56rem. Every size respects the available viewport.
Use `contentProps.className` for application-specific layout overrides.

`contentProps.showCloseButton` defaults to true. Localize the X with
`contentProps.closeButtonLabel` (default `"Close dialog"`). When hiding it, provide another
visible, keyboard-accessible close action. `initialFocus` and `finalFocus` retain the Base UI Popup API.

Content normally unmounts after exit. `contentProps.keepMounted` preserves hidden content and form
state across closure; it does not reset drafts. Reset values explicitly when changing the edited record.

```tsx
<Dialog
  title="Project settings"
  description="Update project details"
  renderHeader={({ title, description }) => (
    <>
      {description}
      <div className="flex items-center gap-2">
        <span aria-hidden="true">✎</span>
        {title}
      </div>
    </>
  )}
>
  <DialogBody>Your settings form</DialogBody>
</Dialog>
```

`renderHeader` receives ready-to-render semantic elements, not raw props. Include the provided
title exactly once and description once when present (`null` when absent). The stationary
header wrapper and X spacing remain owned by Dialog.

## Nested dialogs [#nested-dialogs]

<ComponentPreview name="dialog-demo" title="dialog-demo.tsx">
  <DialogNestedDemo />
</ComponentPreview>

Each Dialog has its own state. `useDialog()` chooses the nearest Dialog through portals;
`useDialog(outerControl)` explicitly selects another. Escape dismisses the topmost dialog.
The parent subtly scales back while a child is open; nested backdrops follow Base UI's behavior.
Nested Dialog and Drawer portals share the outermost panel's theme scope outside its animated panels, so parent
scaling and body overflow do not clip or reposition a child dialog.

## Behavior & Accessibility [#behavior--accessibility]

* A meaningful title names the dialog; the optional description is associated automatically.
* Modal dialogs trap focus, lock page scrolling, and render a backdrop by default.
* `modal={false}` permits outside interaction and omits the backdrop. `modal="trap-focus"`
  traps focus without locking page scroll or blocking outside pointer interaction; it also omits the backdrop.
* Keyboard, Escape, focus targeting/return, dismissal cancellation, and nesting remain Base UI-owned.
* Opening fades, scales from 94%, and lifts the panel by 12px over 320ms; closing reverses the movement
  over 220ms. The backdrop fades over 200ms. Motion's native Web Animations
  integrate with Base UI's animation lifecycle, including rapid reopening and controlled updates.
* Reduced-motion preferences remove scaling, movement, and animated fades. Without Web Animations, state changes are immediate.
* Root props inherit [Base UI Dialog Root](https://base-ui.com/react/components/dialog#root),
  except children and state ownership follow the Vandor contract above. `contentProps` inherits
  Popup props except children; Body and Footer forward native div props and refs.

## Portable Storybook stories [#portable-storybook-stories]

Installing Dialog does not install stories or Storybook packages. Install Dialog first, then optionally:

```bash
npx shadcn@latest add https://vandor-ui.vercel.app/r/dialog-stories.json
```

This separate item installs only `dialog.stories.tsx` beside the component using your configured UI
alias/layout. It does not reinstall or overwrite your customized Dialog, or modify `.storybook/`.
Use an existing React Storybook with `@storybook/react`; keep all Storybook package versions aligned.
The stories are typechecked and composed against Storybook 10.6.1. No addons are required for rendering.
Use your existing Controls setup to edit meaningful Playground args.
Include `**/*.stories.tsx` in your story discovery pattern and import your Tailwind/shadcn global
stylesheet in the preview. Theme decorators belong to the consumer. No Motion provider is required.
Storybook UI is not verified here because this repository has no Storybook server.

## Props [#props]

| Prop                    | Type                                                 | Default         | Description                                                                                                                             |
| ----------------------- | ---------------------------------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| children                | ReactNode \| ((control: DialogControl) => ReactNode) | Not set         | Dialog: plain content or optional render function. Content is not automatically wrapped in DialogBody.                                  |
| closeButtonLabel        | string                                               | "Close dialog"  | contentProps.closeButtonLabel: localized accessible name for the built-in X.                                                            |
| contentProps            | DialogContentProps                                   | Not set         | Popup props, ref, className, render, initialFocus, finalFocus, size, and close/persistence options. No children.                        |
| control                 | DialogControl                                        | Not set         | Dialog: controller from useDialogControl. Exclusive with open, defaultOpen, and handle.                                                 |
| defaultOpen             | boolean                                              | false           | Dialog: initial uncontrolled state.                                                                                                     |
| description             | ReactNode                                            | Not set         | Dialog: optional supporting description.                                                                                                |
| disablePointerDismissal | boolean                                              | false           | Dialog: prevent outside-press dismissal; non-modal focus-out dismissal is also prevented. Escape still works.                           |
| example                 | Example preset                                       | Not applicable  | Playground only: plain children, render function, external control, or native form.                                                     |
| finalFocus              | Base UI Popup finalFocus                             | Base UI default | contentProps.finalFocus: closing focus target. Set explicitly for external openers.                                                     |
| initialFocus            | Base UI Popup initialFocus                           | Base UI default | contentProps.initialFocus: opening focus target, boolean, ref, or interaction-aware function.                                           |
| keepMounted             | boolean                                              | false           | contentProps.keepMounted: retain hidden content and form state after closing.                                                           |
| longContent             | boolean                                              | Not applicable  | Playground only: demonstrate a scrolling body with stationary actions.                                                                  |
| modal                   | boolean \| "trap-focus"                              | true            | Dialog: modal focus and scroll lock. Only true renders a backdrop; trap-focus traps focus without blocking outside pointer interaction. |
| onOpenChange            | (open, details) => void                              | Not set         | Dialog: state request with Base UI reason and details.cancel() for unsaved-change guards.                                               |
| onOpenChangeComplete    | (open: boolean) => void                              | Not set         | Dialog: runs after native Motion animations complete. close() itself returns void.                                                      |
| open                    | boolean                                              | Not set         | Dialog: controlled state; update in onOpenChange to accept requests.                                                                    |
| renderHeader            | (elements: DialogHeaderElements) => ReactNode        | Not set         | Dialog: customize header contents while retaining the provided semantic title and description exactly once.                             |
| showCloseButton         | boolean                                              | true            | contentProps.showCloseButton: show the built-in accessible X.                                                                           |
| size                    | "sm" \| "md" \| "lg" \| "xl"                         | "md"            | contentProps.size: responsive maximum panel width.                                                                                      |
| title                   | ReactNode                                            | Required        | Dialog: required meaningful accessible title.                                                                                           |
| trigger                 | ReactElement                                         | Not set         | Dialog: associated button element. Custom components must forward refs and native props.                                                |

The reference lists component-specific props and selected native props, not the complete React API. Other native attributes are forwarded to the rendered element.

## Source [#source]

```tsx
"use client";

// Adapted from shadcn/ui's Base Nova Dialog (MIT).
// https://ui.shadcn.com/docs/components/base/dialog
/*
MIT License
Copyright (c) 2023 shadcn

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import { Dialog as Primitive } from "@base-ui/react/dialog";
import { useRender } from "@base-ui/react/use-render";
import { cn } from "cn";
import { XIcon } from "lucide-react";
import { useAnimateMini, useReducedMotion } from "motion/react";
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useLayoutEffect,
  useMemo,
  useState,
  useSyncExternalStore,
} from "react";
import type { ComponentProps, ReactElement, ReactNode } from "react";

const controlKey = Symbol("dialog-control");
type DialogHandle = ReturnType<typeof Primitive.createHandle>;

interface DialogControl {
  readonly isOpen: boolean;
  open: () => void;
  close: () => void;
  readonly [controlKey]: {
    handle: DialogHandle;
    subscribe: (listener: () => void) => () => void;
    notify: () => void;
  };
}

const createControl = (handle: DialogHandle): DialogControl => {
  const listeners = new Set<() => void>();
  return {
    [controlKey]: {
      handle,
      notify: () => {
        for (const listener of listeners) {
          listener();
        }
      },
      subscribe: (listener) => {
        listeners.add(listener);
        return () => {
          listeners.delete(listener);
        };
      },
    },
    close: () => handle.close(),
    get isOpen() {
      return handle.isOpen;
    },
    open: () => handle.open(null),
  };
};
const getServerOpen = () => false;
const useSubscription = (control: DialogControl) => {
  useSyncExternalStore(
    control[controlKey].subscribe,
    () => control.isOpen,
    getServerOpen
  );
  return control;
};
const useDialogControl = (): DialogControl => {
  const [control] = useState(() => createControl(Primitive.createHandle()));
  return useSubscription(control);
};
const DialogContext = createContext<DialogControl | null>(null);
const useDialog = (control?: DialogControl): DialogControl => {
  const context = useContext(DialogContext);
  const selected = control ?? context;
  if (!selected) {
    throw new Error(
      "useDialog must be used inside Dialog or receive a control"
    );
  }
  return useSubscription(selected);
};

type DialogContentProps = Omit<Primitive.Popup.Props, "children"> & {
  size?: "sm" | "md" | "lg" | "xl";
  showCloseButton?: boolean;
  closeButtonLabel?: string;
  keepMounted?: boolean;
};
interface DialogHeaderElements {
  title: ReactElement;
  description: ReactElement | null;
}
type DialogStateProps = Omit<
  Primitive.Root.Props,
  "children" | "handle" | "open" | "defaultOpen"
> &
  (
    | {
        control: DialogControl;
        handle?: never;
        open?: never;
        defaultOpen?: never;
      }
    | {
        control?: never;
        handle?: Primitive.Root.Props["handle"];
        open?: boolean;
        defaultOpen?: boolean;
      }
  );
type DialogProps = DialogStateProps & {
  title: ReactNode;
  description?: ReactNode;
  trigger?: ReactElement;
  children?: ReactNode | ((control: DialogControl) => ReactNode);
  contentProps?: DialogContentProps;
  renderHeader?: (elements: DialogHeaderElements) => ReactNode;
};

// Native WAAPI animations remain visible to Base UI's public animation lifecycle.
// This covers controlled prop changes as well as trigger/imperative requests,
// without retaining invisible, focus-trapped panels or overriding completion events.
const AnimatedElement = ({
  nativeProps,
  state,
  render,
  backdrop = false,
}: {
  nativeProps: ComponentProps<"div">;
  state: Primitive.Popup.State | Primitive.Backdrop.State;
  render?: Primitive.Popup.Props["render"];
  backdrop?: boolean;
}) => {
  const [scope, animate] = useAnimateMini<HTMLDivElement>();
  const reduceMotion = useReducedMotion();
  const nestedOpen = "nestedDialogOpen" in state && state.nestedDialogOpen;
  const openScale = nestedOpen ? 0.96 : 1;
  const targetScale = state.open ? openScale : 0.94;
  useLayoutEffect(() => {
    const element = scope.current;
    if (!element) {
      return;
    }
    const opacity = state.open ? 1 : 0;
    const scale = backdrop || reduceMotion ? 1 : targetScale;
    const translate = state.open || reduceMotion ? "0 0px" : "0 12px";
    if (
      nativeProps.hidden ||
      reduceMotion ||
      typeof element.animate !== "function"
    ) {
      element.style.opacity = String(opacity);
      if (!backdrop) {
        element.style.scale = String(scale);
        element.style.translate = translate;
      }
      return;
    }
    const panelDuration = state.open ? 0.32 : 0.22;
    const animation = animate(
      [element],
      backdrop ? { opacity } : { opacity, scale, translate },
      {
        duration: backdrop ? 0.2 : panelDuration,
        ease: state.open ? [0.22, 1, 0.36, 1] : [0.4, 0, 1, 1],
      }
    );
    // WAAPI rejects finished when interrupted; rapid reopen/unmount is expected.
    for (const nativeAnimation of element.getAnimations()) {
      // Intentional interruption is not an application error.
      // eslint-disable-next-line promise/prefer-await-to-then
      nativeAnimation.finished.catch(() => {
        // Interrupted animations are intentionally canceled during cleanup.
      });
    }
    return () => animation.stop();
  }, [
    animate,
    backdrop,
    nativeProps.hidden,
    reduceMotion,
    scope,
    state.open,
    targetScale,
  ]);
  return useRender({
    props: { ...nativeProps },
    ref: [nativeProps.ref ?? null, scope],
    render,
    state: { nested: false, nestedDialogOpen: false, ...state },
  });
};

const DialogChildren = ({
  children,
}: {
  children: DialogProps["children"];
}) => {
  const control = useDialog();
  return <>{typeof children === "function" ? children(control) : children}</>;
};
const DialogToastHost = () => {
  const control = useDialog();
  return (
    <span
      data-vandor-modal-toast-host=""
      data-toast-host-active={control.isOpen ? "true" : "false"}
      ref={(node) => {
        if (!node) return;
        if (control.isOpen && !node.dataset.toastHostOrder)
          node.dataset.toastHostOrder = String(performance.now());
        if (!control.isOpen) delete node.dataset.toastHostOrder;
      }}
      style={{ display: "contents" }}
    />
  );
};
const owners = new WeakMap<DialogControl, object>();
const Dialog = ({
  control,
  handle,
  title,
  description,
  trigger,
  children,
  renderHeader,
  contentProps = {},
  onOpenChange,
  modal = true,
  ...props
}: DialogProps) => {
  const [localHandle] = useState(() => Primitive.createHandle());
  const localControl = useMemo(
    () => createControl(handle ?? localHandle),
    [handle, localHandle]
  );
  const selected = control ?? localControl;
  const [owner] = useState(() => ({}));
  useEffect(() => {
    if (owners.has(selected)) {
      throw new Error(
        "A DialogControl can only be bound to one Dialog at a time"
      );
    }
    owners.set(selected, owner);
    selected[controlKey].notify();
    return () => {
      if (owners.get(selected) === owner) {
        owners.delete(selected);
      }
      queueMicrotask(selected[controlKey].notify);
    };
  }, [owner, selected]);
  useEffect(() => {
    selected[controlKey].notify();
  }, [selected, props.open]);
  if (
    control &&
    (props.open !== undefined ||
      props.defaultOpen !== undefined ||
      handle !== undefined)
  ) {
    throw new Error(
      "Dialog control cannot be combined with open, defaultOpen, or handle"
    );
  }
  const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(
    null
  );
  const setContainer = useCallback((node: HTMLSpanElement | null) => {
    // Dialog and Drawer share the outermost theme host without an install-time
    // dependency on each other. Never portal into a transformed parent panel.
    let host: HTMLElement | null = node;
    let ancestor = node?.parentElement?.closest<HTMLElement>(
      "[data-vandor-overlay-host]"
    );
    while (ancestor) {
      host = ancestor;
      ancestor = ancestor.parentElement?.closest<HTMLElement>(
        "[data-vandor-overlay-host]"
      );
    }
    setPortalContainer(host);
  }, []);
  const {
    size = "md",
    showCloseButton = true,
    closeButtonLabel = "Close dialog",
    keepMounted = false,
    className,
    render,
    ...popupProps
  } = contentProps;
  const elements: DialogHeaderElements = {
    description:
      description === undefined || description === null ? null : (
        <Primitive.Description
          data-slot="dialog-description"
          className="text-sm leading-relaxed text-muted-foreground"
        >
          {description}
        </Primitive.Description>
      ),
    title: (
      <Primitive.Title
        data-slot="dialog-title"
        className="text-base font-medium leading-snug text-foreground"
      >
        {title}
      </Primitive.Title>
    ),
  };
  return (
    <DialogContext.Provider value={selected}>
      <span
        ref={setContainer}
        data-vandor-overlay-host=""
        style={{ display: "contents" }}
      >
        <Primitive.Root
          {...props}
          handle={selected[controlKey].handle}
          modal={modal}
          onOpenChange={(nextOpen, details) => {
            try {
              onOpenChange?.(nextOpen, details);
            } finally {
              queueMicrotask(selected[controlKey].notify);
            }
          }}
        >
          {trigger && (
            <Primitive.Trigger
              data-slot="dialog-trigger"
              className="cursor-pointer disabled:cursor-not-allowed data-disabled:cursor-not-allowed"
              render={trigger}
            />
          )}
          <Primitive.Portal
            container={portalContainer}
            keepMounted={keepMounted}
            // The theme host uses display: contents. A static portal wrapper
            // would become an extra flex/grid item and introduce caller gaps.
            style={{ position: "absolute" }}
          >
            {modal === true && (
              <Primitive.Backdrop
                data-slot="dialog-overlay"
                className="fixed inset-0 z-50 min-h-dvh bg-black/55 opacity-0 data-ending-style:pointer-events-none supports-[-webkit-touch-callout:none]:absolute"
                render={(nativeProps, state) => (
                  <AnimatedElement
                    nativeProps={nativeProps}
                    state={state}
                    backdrop
                  />
                )}
              />
            )}
            <Primitive.Viewport
              data-slot="dialog-viewport"
              className="pointer-events-none fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6"
            >
              <Primitive.Popup
                data-slot="dialog-popup"
                data-size={size}
                data-close-button={showCloseButton ? "" : undefined}
                className={cn(
                  "group/dialog-popup pointer-events-auto relative flex max-h-[calc(100dvh-2rem)] w-full min-h-0 translate-y-3 scale-[0.94] flex-col overflow-hidden rounded-xl border border-border bg-background text-sm text-foreground opacity-0 shadow-xl outline-none sm:max-h-[calc(100dvh-3rem)] data-nested-dialog-open:brightness-95",
                  {
                    "max-w-2xl": size === "lg",
                    "max-w-4xl": size === "xl",
                    "max-w-lg": size === "md",
                    "max-w-sm": size === "sm",
                  },
                  className
                )}
                {...popupProps}
                render={(nativeProps, state) => (
                  <AnimatedElement
                    nativeProps={nativeProps}
                    state={state}
                    render={render}
                  />
                )}
              >
                <div
                  data-slot="dialog-header"
                  className="flex shrink-0 flex-col gap-2 p-4 pb-0 group-data-close-button/dialog-popup:pr-14"
                >
                  {renderHeader ? (
                    renderHeader(elements)
                  ) : (
                    <>
                      {elements.title}
                      {elements.description}
                    </>
                  )}
                </div>
                <DialogChildren>{children}</DialogChildren>
                {showCloseButton && (
                  <Primitive.Close
                    data-slot="dialog-close-button"
                    aria-label={closeButtonLabel}
                    className="absolute top-3 right-3 inline-flex size-8 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none"
                  >
                    <XIcon aria-hidden="true" className="size-4" />
                  </Primitive.Close>
                )}
              </Primitive.Popup>
            </Primitive.Viewport>
            {modal === true && <DialogToastHost />}
          </Primitive.Portal>
        </Primitive.Root>
      </span>
    </DialogContext.Provider>
  );
};
const DialogBody = ({ className, ...props }: ComponentProps<"div">) => (
  <div
    data-slot="dialog-body"
    className={cn(
      "min-h-0 flex-1 overflow-y-auto overscroll-contain p-4",
      className
    )}
    {...props}
  />
);
const DialogFooter = ({ className, ...props }: ComponentProps<"div">) => (
  <div
    data-slot="dialog-footer"
    className={cn(
      "mt-auto flex shrink-0 flex-col-reverse gap-2 p-4 pt-0 sm:flex-row sm:justify-end",
      className
    )}
    {...props}
  />
);

export { Dialog, DialogBody, DialogFooter, useDialog, useDialogControl };
export type {
  DialogControl,
  DialogHeaderElements,
  DialogProps,
  DialogContentProps,
};

```

## Credits [#credits]

* [shadcn/ui](https://ui.shadcn.com/docs/components/base/dialog): Adapted Base Nova Dialog source and semantic composition (MIT). Vandor adds the panel API, controllers, scroll layout, and Motion integration.

* [Base UI](https://base-ui.com/react/components/dialog): Accessible dialog primitive, focus management, dismissal, and nested-dialog behavior.
