Dialog
Installation
import { Dialog } from "@o/pipeline/components/dialog";
Implementation
Anatomy
Our Dialog component is built upon the Base UI Dialog component, with additional components:
Dialog.RootDialog.Trigger- primitive button that opens the dialog. Fully customizable via therenderprop.Dialog.PortalDialog.BackdropDialog.ViewportDialog.PopupDialog.HeaderDialog.HeaderClose
Dialog.BodyDialog.Title- obligatory, but can be set tovisuallyHiddenDialog.Description- optional, can be set tovisuallyHidden
Dialog.FooterDialog.FooterAction- can contain more than one action
Dialog.Close- primitive close action thatDialog.HeaderCloseextends upon. Fully customizable via therenderprop.
Layout
Dialog’s core layout is fixed and non-customizable; the Dialog.Header, Dialog.Body, and Dialog.Footer will always render in the same order.
Additionally, Dialog.Header and Dialog.Footer’s contents will ensure Dialog.HeaderClose and Dialog.FooterAction are always rendered in the same position, regardless of additional content.
Intent
Default
Danger
Examples
Dynamic Content
Migration from Legacy Dialog
Import Changes
// Before (legacy full-screen dialog)
import { LegacyDialog } from "@o/pipeline/components/legacy/dialog";
// After (new modal dialog)
import { Dialog } from "@o/pipeline/components/dialog";
Component Mapping
| Legacy | Current |
|---|---|
Dialog.Content | Dialog.Body |
Dialog.Breadcrumb | (removed, use Header) |
Dialog.Form | (use standard form) |
Dialog.FormInput | (use standard Input) |
Dialog.FormField | (use standard label) |
Dialog.Action | Button in FooterAction |
Dialog.Cancel | Dialog.Close with Button |
Dialog.Header | Dialog.Header |
Dialog.Footer | Dialog.Footer |
Dialog.Title | Dialog.Title |
Dialog.Description | Dialog.Description |
Example Migration
Before (Legacy):
<Dialog.Root>
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Popup>
<Dialog.Close />
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Delete Item</Dialog.Title>
</Dialog.Header>
<p>Are you sure?</p>
<Dialog.Footer>
<Dialog.Cancel>Cancel</Dialog.Cancel>
<Dialog.Action variant="danger">Delete</Dialog.Action>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
After (New):
<Dialog.Root intent="danger">
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Popup>
<Dialog.Header>
<Dialog.Title>Delete Item</Dialog.Title>
<Dialog.HeaderClose />
</Dialog.Header>
<Dialog.Body>
<Dialog.Description>Are you sure?</Dialog.Description>
</Dialog.Body>
<Dialog.Footer>
<Dialog.FooterAction>
<Dialog.Close render={<Button variant="secondary">Cancel</Button>} />
<Button variant="danger">Delete</Button>
</Dialog.FooterAction>
</Dialog.Footer>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
Common Patterns
Standalone Dialog with Jotai State
For dialogs that need to be opened programmatically from anywhere in the app:
import { Dialog } from "@o/pipeline/components/dialog";
import { Button } from "@o/pipeline/components/button";
import { atom, useAtom } from "jotai";
import { appStore } from "@/lib/jotai-store";
// 1. Define state interface and atom
interface MyDialogState {
isOpen: boolean;
props: { title: string; onConfirm: () => void };
}
const myDialogAtom = atom<MyDialogState>({
isOpen: false,
props: { title: "", onConfirm: () => {} },
});
// 2. Export function to open dialog from anywhere
export function openMyDialog(props: MyDialogState["props"]) {
appStore.set(myDialogAtom, { isOpen: true, props });
}
// 3. Create dialog component (mount once in providers)
export function MyDialog() {
const [state, setState] = useAtom(myDialogAtom);
const { isOpen, props } = state;
const setOpen = (open: boolean) => {
setState((prev) => ({ ...prev, isOpen: open }));
};
return (
<Dialog.Root open={isOpen} onOpenChange={setOpen}>
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Popup>
<Dialog.Header>
<Dialog.Title>{props.title}</Dialog.Title>
<Dialog.HeaderClose />
</Dialog.Header>
<Dialog.Body>
<Dialog.Description>Content here</Dialog.Description>
</Dialog.Body>
<Dialog.Footer>
<Dialog.FooterAction>
<Dialog.Close
render={<Button variant="secondary">Cancel</Button>}
/>
<Button onClick={props.onConfirm}>Confirm</Button>
</Dialog.FooterAction>
</Dialog.Footer>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
);
}
Loading State in Footer Actions
Use the disabled prop on buttons during async operations:
const [isPending, startTransition] = useTransition();
<Dialog.Footer>
<Dialog.FooterAction>
<Dialog.Close
render={
<Button variant="secondary" disabled={isPending}>
Cancel
</Button>
}
/>
<Button variant="danger" onClick={onSubmit} disabled={isPending}>
{isPending ? "Deleting…" : "Delete"}
</Button>
</Dialog.FooterAction>
</Dialog.Footer>;
Danger Intent
Use intent="danger" on the Dialog.Root for destructive actions. This applies a subtle red overlay to the backdrop:
<Dialog.Root intent="danger" open={isOpen} onOpenChange={setOpen}>
{/* Dialog content */}
</Dialog.Root>