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.Root
  • Dialog.Trigger - primitive button that opens the dialog. Fully customizable via the render prop.
  • Dialog.Portal
    • Dialog.Backdrop
    • Dialog.Viewport
    • Dialog.Popup
      • Dialog.Header
        • Dialog.HeaderClose
      • Dialog.Body
        • Dialog.Title - obligatory, but can be set to visuallyHidden
        • Dialog.Description - optional, can be set to visuallyHidden
      • Dialog.Footer
        • Dialog.FooterAction - can contain more than one action
      • Dialog.Close - primitive close action that Dialog.HeaderClose extends upon. Fully customizable via the render prop.

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

LegacyCurrent
Dialog.ContentDialog.Body
Dialog.Breadcrumb(removed, use Header)
Dialog.Form(use standard form)
Dialog.FormInput(use standard Input)
Dialog.FormField(use standard label)
Dialog.ActionButton in FooterAction
Dialog.CancelDialog.Close with Button
Dialog.HeaderDialog.Header
Dialog.FooterDialog.Footer
Dialog.TitleDialog.Title
Dialog.DescriptionDialog.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>
  );
}

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>