Siteplane UI

Dialog

Display a focused task in an overlay with a title and a clear close action.

Markdown
"use client";

import { Button } from "@/siteplane/components/button";
import {
  Dialog,
  DialogClose,
  DialogPopup,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPanel,
  DialogTitle,
  DialogTrigger,
} from "@/siteplane/components/dialog";
import { Field, FieldError, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";
import { Input } from "@/siteplane/components/input";
import { useState } from "react";

export default function Example() {
  const [open, setOpen] = useState(false);
  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger render={<Button variant="outline" />}>
        Rename project
      </DialogTrigger>
      <DialogPopup>
        <Form
          onSubmit={(event) => {
            event.preventDefault();
            setOpen(false);
          }}
        >
          <DialogHeader>
            <DialogTitle>Rename project</DialogTitle>
            <DialogDescription>
              Everyone in the workspace will see the new name.
            </DialogDescription>
          </DialogHeader>
          <DialogPanel>
            <Field name="name">
              <FieldLabel>Project name</FieldLabel>
              <Input defaultValue="Website Redesign" required />
              <FieldError match="valueMissing">
                Enter a project name.
              </FieldError>
            </Field>
          </DialogPanel>
          <DialogFooter>
            <DialogClose render={<Button variant="ghost" />}>
              Cancel
            </DialogClose>
            <Button type="submit">Save</Button>
          </DialogFooter>
        </Form>
      </DialogPopup>
    </Dialog>
  );
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/dialog

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/data-list @siteplane/form @siteplane/input @siteplane/label @siteplane/menu

Field is installed with Input.

Usage

import { Button } from "@/siteplane/components/button";
import {
  Dialog,
  DialogTrigger,
  DialogPopup,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose,
} from "@/siteplane/components/dialog";

<Dialog>
  <DialogTrigger render={<Button />}>Open dialog</DialogTrigger>
  <DialogPopup>
    <DialogHeader>
      <DialogTitle>Project details</DialogTitle>
      <DialogDescription>Review the current project.</DialogDescription>
    </DialogHeader>
    <DialogFooter>
      <DialogClose render={<Button />}>Done</DialogClose>
    </DialogFooter>
  </DialogPopup>
</Dialog>;

Composition

Dialog
├── DialogTrigger
└── DialogPopup
    ├── DialogHeader
    │   ├── DialogTitle
    │   └── DialogDescription
    ├── DialogPanel
    └── DialogFooter
        └── DialogClose

DialogPopup includes the portal, backdrop, viewport, popup, and close icon. Do not add a second portal or backdrop. DialogContent and DialogOverlay are the shadcn names of DialogPopup and DialogBackdrop; both keep working.

Size

DialogPopup takes size, the width on the kit's modal scale: xs 400 px, sm 440 px (the default), md 600 px or lg 880 px. Use md or lg for a form with two columns or a wide table. The size applies from 640 px up; below that, every dialog is a full-width sheet at the bottom edge. A max-w-* class in className still sets its own width, also below 640 px. Alert Dialog takes the same sizes.

Open from a menu

Pass open and onOpenChange when something other than DialogTrigger opens the dialog, such as a menu item. Render the dialog outside the menu so it stays mounted after the menu closes.

"use client";

import { CopyIcon, ShareIcon } from "lucide-react";
import { Button } from "@/siteplane/components/button";
import {
  Dialog,
  DialogClose,
  DialogPopup,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPanel,
  DialogTitle,
} from "@/siteplane/components/dialog";
import { Input } from "@/siteplane/components/input";
import { Label } from "@/siteplane/components/label";
import {
  Menu,
  MenuItem,
  MenuPopup,
  MenuTrigger,
} from "@/siteplane/components/menu";
import { useId, useState } from "react";

export default function Example() {
  const linkId = useId();
  const [open, setOpen] = useState(false);
  return (
    <>
      <Menu>
        <MenuTrigger render={<Button variant="outline" />}>
          Project actions
        </MenuTrigger>
        <MenuPopup align="start">
          <MenuItem>
            <CopyIcon />
            Duplicate
          </MenuItem>
          <MenuItem onClick={() => setOpen(true)}>
            <ShareIcon />
            Share…
          </MenuItem>
        </MenuPopup>
      </Menu>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogPopup>
          <DialogHeader>
            <DialogTitle>Share project</DialogTitle>
            <DialogDescription>
              Anyone in the workspace with this link can view the project.
            </DialogDescription>
          </DialogHeader>
          <DialogPanel className="grid gap-3">
            <Label htmlFor={linkId}>Project link</Label>
            <Input
              id={linkId}
              defaultValue="https://app.example.com/projects/website-redesign"
              readOnly
            />
          </DialogPanel>
          <DialogFooter>
            <DialogClose render={<Button />}>Done</DialogClose>
          </DialogFooter>
        </DialogPopup>
      </Dialog>
    </>
  );
}

Nested dialogs

Open a dialog from inside another dialog. The parent stays visible behind it, scaled down and faded by 10 % for each dialog on top, so the context stays in view. Each root owns its state; Escape closes the topmost dialog, and closing it returns focus to its trigger in the parent.

"use client";

import { Button } from "@/siteplane/components/button";
import {
  DataList,
  DataListItem,
  DataListLabel,
  DataListValue,
} from "@/siteplane/components/data-list";
import {
  Dialog,
  DialogClose,
  DialogPopup,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPanel,
  DialogTitle,
  DialogTrigger,
} from "@/siteplane/components/dialog";
import { Field, FieldError, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";
import { Input } from "@/siteplane/components/input";
import { useState } from "react";

export default function Example() {
  const [editOpen, setEditOpen] = useState(false);
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>
        Manage member
      </DialogTrigger>
      <DialogPopup>
        <DialogHeader>
          <DialogTitle>Manage team member</DialogTitle>
          <DialogDescription>
            View and update this member of your workspace.
          </DialogDescription>
        </DialogHeader>
        <DialogPanel>
          <DataList orientation="vertical">
            <DataListItem>
              <DataListLabel>Name</DataListLabel>
              <DataListValue>Ben Carter</DataListValue>
            </DataListItem>
            <DataListItem>
              <DataListLabel>Email</DataListLabel>
              <DataListValue>ben.carter@example.com</DataListValue>
            </DataListItem>
          </DataList>
        </DialogPanel>
        <DialogFooter>
          <DialogClose render={<Button variant="ghost" />}>Close</DialogClose>
          <Dialog open={editOpen} onOpenChange={setEditOpen}>
            <DialogTrigger render={<Button />}>Edit details</DialogTrigger>
            <DialogPopup>
              <Form
                onSubmit={(event) => {
                  event.preventDefault();
                  setEditOpen(false);
                }}
              >
                <DialogHeader>
                  <DialogTitle>Edit details</DialogTitle>
                  <DialogDescription>
                    Update the member&apos;s name and email address.
                  </DialogDescription>
                </DialogHeader>
                <DialogPanel className="grid gap-4">
                  <Field name="name">
                    <FieldLabel>Name</FieldLabel>
                    <Input defaultValue="Ben Carter" required />
                    <FieldError match="valueMissing">Enter a name.</FieldError>
                  </Field>
                  <Field name="email">
                    <FieldLabel>Email</FieldLabel>
                    <Input
                      type="email"
                      defaultValue="ben.carter@example.com"
                      required
                    />
                    <FieldError>Enter a valid email address.</FieldError>
                  </Field>
                </DialogPanel>
                <DialogFooter>
                  <DialogClose render={<Button variant="ghost" />}>
                    Cancel
                  </DialogClose>
                  <Button type="submit">Save changes</Button>
                </DialogFooter>
              </Form>
            </DialogPopup>
          </Dialog>
        </DialogFooter>
      </DialogPopup>
    </Dialog>
  );
}

Scrollable content

Put long content in DialogPanel. When the dialog reaches the available height, the panel scrolls while the header and footer stay in view. The dialog opens at the top of its content: when its first focusable element, such as a confirmation field under long terms, starts below the visible part of the panel, the popup itself takes focus, and Tab moves on to that element.

"use client";

import { Button } from "@/siteplane/components/button";
import {
  Dialog,
  DialogClose,
  DialogPopup,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPanel,
  DialogTitle,
  DialogTrigger,
} from "@/siteplane/components/dialog";

const sections = [
  {
    title: "Your account",
    text: "You are responsible for activity in your account and for keeping your sign-in details secure. Tell us right away if you notice access you don't recognize.",
  },
  {
    title: "Workspace content",
    text: "You keep ownership of the projects, files and comments you add. We store and process them only to provide the service to your workspace.",
  },
  {
    title: "Acceptable use",
    text: "Don't upload malicious code, try to access other workspaces, or use the service to send unsolicited messages.",
  },
  {
    title: "Plans and billing",
    text: "Paid plans renew automatically at the end of each billing period. You can cancel at any time, and access continues until the period ends.",
  },
  {
    title: "Deleted projects",
    text: "Deleted projects stay in the trash for 30 days. After that, they are removed permanently and can't be restored.",
  },
  {
    title: "Service availability",
    text: "We may pause parts of the service for maintenance. Planned maintenance is announced to workspace owners in advance.",
  },
  {
    title: "Changes to these terms",
    text: "We notify workspace owners at least 30 days before a material change takes effect. Continuing to use the service means you accept the updated terms.",
  },
  {
    title: "Closing your account",
    text: "You can export your data and close your account from the account settings. Workspace owners can also remove members at any time.",
  },
  {
    title: "Contact",
    text: "Send questions about these terms to your workspace administrator or contact support from the help menu.",
  },
];

export default function Example() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>
        Review terms
      </DialogTrigger>
      <DialogPopup showCloseButton={false}>
        <DialogHeader>
          <DialogTitle>Updated terms of service</DialogTitle>
          <DialogDescription>
            Review the changes before you continue.
          </DialogDescription>
        </DialogHeader>
        <DialogPanel className="grid gap-4">
          {sections.map((section) => (
            <div key={section.title} className="grid gap-1 text-sm">
              <h3 className="font-medium">{section.title}</h3>
              <p className="text-muted-foreground">{section.text}</p>
            </div>
          ))}
        </DialogPanel>
        <DialogFooter>
          <DialogClose render={<Button variant="ghost" />}>Cancel</DialogClose>
          <DialogClose render={<Button />}>Accept</DialogClose>
        </DialogFooter>
      </DialogPopup>
    </Dialog>
  );
}

API reference

Dialog

The root renders no DOM element. These properties retain the installed Base UI contract.

Component properties
PropTypeDefaultDescription
openbooleanuncontrolledControlled open state.
defaultOpenbooleanfalseInitial uncontrolled open state.
onOpenChange(open, eventDetails) => voidunsetOpen/close request. Details include reason, event, cancel(), and preventUnmountOnClose().
onOpenChangeComplete(open: boolean) => voidunsetRuns after opening or closing animations complete.
modalboolean | "trap-focus"truetrue traps focus, locks page scrolling and blocks outside interaction. false permits outside interaction. trap-focus traps focus without the other modal restrictions.
disablePointerDismissalbooleanfalsePrevents outside-press dismissal; also prevents focus-out dismissal in non-modal mode. Escape still closes the dialog.
actionsRefRefObject<DialogPrimitive.Root.Actions | null>unsetImperative close() and unmount(); manual unmount is for externally managed exit animations.
handleDialog handleunsetConnects detached triggers and imperative handle methods.
triggerIdstring | nullunsetIdentifies the active trigger when controlling open state with multiple triggers.
defaultTriggerIdstring | nullunsetInitial trigger association, for example with defaultOpen.
childrenReactNode | ({ payload }) => ReactNodeunsetParts, or a render function receiving the active trigger payload.

DialogPopup / DialogContent

Component properties
PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg"smPopup width from 640 px up: 400, 440, 600 or 880 px.
showCloseButtonbooleantrueRenders the built-in close icon action. When false, provide another accessible close action.
closePropsDialogPrimitive.Close.PropsunsetOverrides the built-in close action, including aria-label (default: Close).
portalPropsDialogPrimitive.Portal.PropsunsetConfigures the included portal; container defaults to document.body, keepMounted to false.
initialFocusboolean | RefObject | (interactionType) => targetfirst tabbable elementFocus target when opening. By default the first tabbable element, or the popup itself on touch and when that element starts out of view.
finalFocusboolean | RefObject | (interactionType) => targetfoundation defaultFocus target when closing.
classNamestringunsetAdds popup styling.
renderReactElement | render functiondivComposes the popup onto another element.
childrenReactNodeunsetPopup content before the optional built-in close button.

The styled parts take a string className, which they merge with their own classes through cn. Style a state through the part's data attributes, such as data-open, rather than a className function.

The remaining popup props follow Base UI Popup. Keep the portal in the default location unless there is a concrete container requirement. The supplied composition carries the Siteplane UI provider settings into its portal.

Content layout parts

Component properties
PropTypeDefaultDescription
DialogHeaderdiv props + renderdivGroups title/description with header spacing. className adds local styling.
DialogFooterdiv props + renderdivLays out actions in a row, aligned to the end.
DialogPanel.scrollFadebooleantrueEnables the scroll area edge fade.
DialogPaneldiv props + renderdiv inside ScrollAreaBody wrapped in a ScrollArea; className styles the inner panel. When the content exceeds the available height, it scrolls between the header and footer.
DialogTitleDialogPrimitive.Title.Propsh2Accessible dialog title with heading typography: 18 px in the title weight, --sp-rw-font-weight-title (450), which Theming lists.
DialogDescriptionDialogPrimitive.Description.PropspAccessible description with secondary text styling.

All layout parts accept children, className, native props and render through their corresponding contract. Use React.ComponentProps<typeof DialogPanel> (or another part) to derive its TypeScript props; Siteplane UI does not export separate named prop aliases for these wrappers.

Trigger, close and lower-level exports

Component properties
PropTypeDefaultDescription
DialogTriggerDialogPrimitive.Trigger.PropsbuttonOpens its root. Supports render, disabled, id, handle and payload.
DialogCloseDialogPrimitive.Close.PropsbuttonRequests closing. Supports render and disabled.
DialogPortalDialogPrimitive.Portal.Propsbody / keepMounted=falseMoves content to a container. Already included in DialogPopup.
DialogBackdrop / DialogOverlayDialogPrimitive.Backdrop.PropsdivStyled overlay; already included in DialogPopup.
DialogViewportDialogPrimitive.Viewport.PropsdivStyled overlay layout container; already included in DialogPopup.
DialogPrimitiveBase UI Dialog namespace—Public foundation namespace for advanced composition and its types. It is not the styled Siteplane UI popup.
DialogCreateHandle<Payload>()() => Dialog handle—Creates a handle; retain the instance with state or another stable owner.

Share a stable DialogCreateHandle between the root and detached triggers; the root's render function receives the active trigger's payload. The handle exposes open(triggerId | null), openWithPayload(payload), close(), and isOpen. Call methods in an event handler or effect after the associated root has mounted. Controlled roots still need their state updated through onOpenChange.

Use the ready-made DialogPopup for normal usage. A custom low-level composition must supply its own popup, close action, provider scope and positioning; do not combine a second low-level portal/backdrop with DialogPopup. In particular, setting modal={false} does not remove the backdrop or full-screen viewport included by the styled composition. For an anchored, non-blocking interaction use the appropriate Popover component.

Full inherited part contracts: Root, Trigger, Portal, Backdrop, Viewport, Title, Description, and Close. The installed source is authoritative for the version in your project.

Usage notes

Provide DialogTitle on every dialog, and a description when it adds useful context. Label every form control. Tab and Shift+Tab stay within a modal dialog; Escape requests closing. Always retain an accessible close action, including when hiding the default icon. Verify focus restoration when the original trigger is removed from the page.

Opening focuses the first tabbable element. The popup itself takes focus instead on touch devices, so the on-screen keyboard doesn't open, and when that element starts out of view in a scrolling panel, so a long dialog opens at its top and a screen reader reads its title first. Closing returns focus to the trigger or the previously focused element; use initialFocus and finalFocus to choose other targets.

Submit forms with Button type="submit" and close the dialog only after the submission succeeds; a submit button wrapped in DialogClose would close immediately. Keep the dialog open while an asynchronous request is pending or has failed. To confirm before discarding unsaved changes, see Close confirmation.

When the user must make a decision before going on, use Alert Dialog instead. Keep actions and scrollable content reachable at mobile widths and with zoom. The kit carries shared size, theme, and motion settings through its normal portal; no additional provider is required by these examples.