Dialog
A modal window that overlays the main content, requiring user interaction before returning to the application.
Overview
The Dialog component is a modal window that displays content on top of the main application, requiring user interaction before continuing. Built on Base UI primitives, it features smooth animations, backdrop blur effects, sticky headers and footers for scrollable content, and flexible sizing options including fullscreen mode.
Usage
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogBody,
DialogFooter,
DialogClose,
DialogAction,
} from "@tilt-legal/cubitt-components/primitives";<Dialog>
<DialogTrigger render={<Button />}>Open Dialog</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>Dialog description goes here.</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>Using Cubitt's URL-state hooks, you can sync the dialog state with the URL by providing a paramName:
// Opens when ?settings=true in URL
<Dialog paramName="settings">
<DialogTrigger render={<Button />}>Open Settings</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>
Configure your preferences.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
// Advanced options:
<Dialog
paramName="dialog"
paramClearOnDefault={true} // Remove param when closed
onUrlValueChange={(value) => console.log('Dialog open:', value)}
>
{/* ... */}
</Dialog>Using String Values (e.g., IDs)
Dialog URL state always serializes to strings. There are two modes:
Mode 1: Open for Any Value (no paramMatchValue specified)
// Opens when ANY value is present: ?dialog=true, ?dialog=abc, etc.
// Useful when you just need to track that dialog was opened via URL
<Dialog
paramName="dialog"
onUrlValueChange={(value) => {
console.log("URL value:", value); // Gets the actual string value
}}
>
<DialogContent>{/* ... */}</DialogContent>
</Dialog>Mode 2: Match Specific Value (with paramMatchValue)
This mode is useful when you have multiple dialogs and want each to respond to a specific ID:
import { Button } from "@tilt-legal/cubitt-components/primitives";
// Button sets item ID
<Button paramName="inspect-dialog" paramSetValue="item-001">
Inspect Item 001
</Button>
// Dialog opens ONLY when ?inspect-dialog=item-001
<Dialog
paramName="inspect-dialog"
paramMatchValue="item-001" // ← Must match this specific value
>
<DialogContent>
<DialogHeader>
<DialogTitle>Inspector: Item 001</DialogTitle>
<DialogDescription>
This dialog only opens for item-001!
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>This allows multiple dialogs with different paramMatchValue props to coexist - each only opens when the URL matches their specific value.
Examples
Dialog with Form
Dialog containing form inputs for data entry.
<Dialog>
<DialogTrigger render={<Button />}>Edit Profile</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" defaultValue="John Doe" />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" defaultValue="john@example.com" />
</div>
</div>
</DialogBody>
<DialogFooter>
<DialogClose>Cancel</DialogClose>
<DialogAction>Save Changes</DialogAction>
</DialogFooter>
</DialogContent>
</Dialog>Scrollable Content
Dialog with long content that scrolls. The header and footer remain sticky with a blurred background effect.
<Dialog>
<DialogTrigger render={<Button />}>Terms & Conditions</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Terms and Conditions</DialogTitle>
<DialogDescription>
Please read and accept our terms and conditions.
</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="space-y-4">{/* Long content... */}</div>
</DialogBody>
<DialogFooter>
<DialogClose>Decline</DialogClose>
<DialogAction>Accept</DialogAction>
</DialogFooter>
</DialogContent>
</Dialog>The scrollbar track is automatically inset to clear the sticky header and footer areas. This is controlled via CSS custom properties --scrollbar-inset-top and --scrollbar-inset-bottom, which default to 3.5rem inside DialogContent. You can override these per-dialog if your header or footer has a non-standard height:
<DialogContent className="[--scrollbar-inset-top:5rem] [--scrollbar-inset-bottom:4rem]">
{/* ... */}
</DialogContent>Fullscreen Dialog
Dialog that takes up most of the screen, useful for complex forms or detailed content.
<Dialog>
<DialogTrigger render={<Button />}>Open Fullscreen Dialog</DialogTrigger>
<DialogContent fullscreen>
<DialogHeader>
<DialogTitle>Fullscreen Dialog</DialogTitle>
<DialogDescription>
This dialog takes up most of the screen space.
</DialogDescription>
</DialogHeader>
<DialogBody>
<p>Content goes here...</p>
</DialogBody>
<DialogFooter>
<DialogClose>Close</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>Without Dismiss Button
Dialog without the X close button in the top right corner.
<Dialog>
<DialogTrigger render={<Button />}>Open Without Dismiss Button</DialogTrigger>
<DialogContent showDismissButton={false}>
<DialogHeader>
<DialogTitle>No Dismiss Button</DialogTitle>
<DialogDescription>
This dialog doesn't have the X close button in the top right.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose>Close</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>Nested Dialogs
Open a dialog from within another dialog. The parent dialog will scale down and shift to create a visual hierarchy.
<Dialog>
<DialogTrigger render={<Button />}>Manage Team Member</DialogTrigger>
<DialogContent showDismissButton={false}>
<DialogHeader>
<DialogTitle>Manage Team Member</DialogTitle>
<DialogDescription>
View and manage a user in your team.
</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="grid gap-4">
<div className="grid gap-1">
<p className="text-muted-foreground text-sm">Name</p>
<p className="font-medium text-sm">Sarah Anderson</p>
</div>
<div className="grid gap-1">
<p className="text-muted-foreground text-sm">Email</p>
<p className="font-medium text-sm">sarah.anderson@company.com</p>
</div>
</div>
</DialogBody>
<DialogFooter>
{/* Nested Dialog */}
<Dialog>
<DialogTrigger render={<Button variant="secondary" />}>
Edit Details
</DialogTrigger>
<DialogContent showDismissButton={false}>
<DialogHeader>
<DialogTitle>Edit Details</DialogTitle>
<DialogDescription>
Make changes to the member's information.
</DialogDescription>
</DialogHeader>
<DialogBody>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<Label htmlFor="name">Name</Label>
<Input defaultValue="Sarah Anderson" id="name" />
</div>
<div className="flex flex-col gap-1">
<Label htmlFor="email">Email</Label>
<Input defaultValue="sarah.anderson@company.com" id="email" />
</div>
</div>
</DialogBody>
<DialogFooter>
<DialogClose>Cancel</DialogClose>
<DialogAction>Save Changes</DialogAction>
</DialogFooter>
</DialogContent>
</Dialog>
<DialogClose>Close</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>URL State
Dialog with URL state synchronization for shareable dialog states.
import {
Dialog,
DialogAction,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Button,
} from "@tilt-legal/cubitt-components/primitives";
// Button sets URL param directly, Dialog reads it
<>
<Button variant="outline" paramName="settings" paramSetValue="true">
Open Settings
</Button>
<Dialog paramName="settings">
<DialogContent>
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>
This dialog's state is synced with the URL. Check the URL parameter
when you open it!
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose>Cancel</DialogClose>
<DialogAction>Save</DialogAction>
</DialogFooter>
</DialogContent>
</Dialog>
</>;API Reference
Dialog
Root component that manages dialog state.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | - | Controlled open state of the dialog. |
defaultOpen | boolean | false | Default open state for uncontrolled usage. |
onOpenChange | (open: boolean) => void | - | Callback fired when the open state changes. |
modal | boolean | true | Whether the dialog is modal (locks scroll). |
disabled | boolean | false | Whether the dialog is disabled. |
URL State Props
When provided with a paramName, the dialog will sync its open/closed state with URL parameters via TanStack Router search params.
| Prop | Type | Default | Description |
|---|---|---|---|
paramName | string | — | URL parameter name for syncing state with URL. Enables URL state management. |
paramMatchValue | string | — | Value that must match for the dialog to open. |
onUrlValueChange | (value: string) => void | — | Callback when URL parameter value changes. |
paramClearOnDefault | boolean | true | Remove URL param when dialog is closed. |
paramThrottle | number | — | Throttle URL updates in milliseconds. |
paramDebounce | number | — | Debounce URL updates in milliseconds. |
DialogTrigger
Button that opens the dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
render | React.ReactElement | - | Custom trigger element to render. |
disabled | boolean | false | Whether the trigger is disabled. |
DialogContent
Container for the dialog content with backdrop and animations.
| Prop | Type | Default | Description |
|---|---|---|---|
showBackdrop | boolean | true | Whether to show the backdrop overlay. |
showDismissButton | boolean | true | Whether to show the X close button in the top right. |
fullscreen | boolean | false | Whether the dialog should take up most of the screen. |
nestedOffset | boolean | true | Whether to apply offset transforms for nested dialogs based on nesting depth. |
className | string | - | Additional CSS classes for the dialog popup. |
CSS Custom Properties
| Variable | Default | Description |
|---|---|---|
--scrollbar-inset-top | 3.75rem | Top margin of the scrollbar track (clears sticky header) |
--scrollbar-inset-bottom | 4rem | Bottom margin of the scrollbar track (clears sticky footer) |
DialogHeader
Container for dialog title and description with sticky positioning.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the header. |
DialogTitle
The accessible title of the dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the title. |
DialogDescription
Description text that provides context about the dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the description. |
DialogBody
Container for the main dialog content.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the body. |
DialogFooter
Container for dialog actions with sticky positioning.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the footer. |
DialogClose
Button that closes the dialog (styled as outline button by default).
| Prop | Type | Default | Description |
|---|---|---|---|
render | React.ReactElement | - | Custom close button element to render. |
className | string | - | Additional CSS classes for the close button. |
DialogAction
Primary action button that closes the dialog (styled as default button).
| Prop | Type | Default | Description |
|---|---|---|---|
render | React.ReactElement | - | Custom action button element to render. |
className | string | - | Additional CSS classes for the action button. |