Skip to main content

Dialog

Dialog wraps Spar's headless dialog primitive and adds Takeoff styling hooks for overlay intensity, header types, and compound anatomy.

Usage​

import { Dialog } from '@takeoff-ui/react-spar';
<Dialog>
<Dialog.Trigger />
<Dialog.Overlay />
<Dialog.Panel>
<Dialog.Header>
<Dialog.Title />
<Dialog.Close />
</Dialog.Header>
<Dialog.Body>
<Dialog.Description />
</Dialog.Body>
<Dialog.Footer />
</Dialog.Panel>
</Dialog>

Playground​


function PlaygroundDemo() {
  return (
    <Dialog>
      <Dialog.Trigger as={Button}>Open Dialog</Dialog.Trigger>
      <Dialog.Overlay />
      <Dialog.Panel>
        <Dialog.Header>
          <Dialog.Title>Flight Details</Dialog.Title>
          <Dialog.Close />
        </Dialog.Header>
        <Dialog.Body>
          <Dialog.Description>
            Review your selected flight information before continuing.
          </Dialog.Description>
        </Dialog.Body>
      </Dialog.Panel>
    </Dialog>
  );
}

render(<PlaygroundDemo />);

Intensity​

Use intensity to control how strong the backdrop appears behind the dialog.


function OverlayIntensityDemo() {
  const intensities = ['lightest', 'light', 'base', 'dark', 'darkest'];

  return (
    <div className="flex flex-wrap justify-center gap-3">
      {intensities.map((intensity) => (
        <Dialog key={intensity}>
          <Dialog.Trigger as={Button}>{intensity}</Dialog.Trigger>
          <Dialog.Overlay intensity={intensity} />
          <Dialog.Panel>
            <Dialog.Header>
              <Dialog.Title>{intensity} overlay</Dialog.Title>
              <Dialog.Close />
            </Dialog.Header>
            <Dialog.Body>
              Overlay intensity is set to <code>{intensity}</code>.
            </Dialog.Body>
          </Dialog.Panel>
        </Dialog>
      ))}
    </div>
  );
}

render(<OverlayIntensityDemo />);

Overlay Behavior​

Use blur for a softened backdrop and invisible when the overlay should stay interactive but visually disappear.


function OverlayBehaviorDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-3">
      <Dialog>
        <Dialog.Trigger as={Button}>Blur</Dialog.Trigger>
        <Dialog.Overlay blur />
        <Dialog.Panel>
          <Dialog.Header>
            <Dialog.Title>Blurred overlay</Dialog.Title>
            <Dialog.Close />
          </Dialog.Header>
          <Dialog.Body>
            The backdrop stays visible and adds a soft blur effect behind the panel.
          </Dialog.Body>
        </Dialog.Panel>
      </Dialog>

      <Dialog>
        <Dialog.Trigger as={Button}>Invisible</Dialog.Trigger>
        <Dialog.Overlay invisible />
        <Dialog.Panel>
          <Dialog.Header>
            <Dialog.Title>Invisible overlay</Dialog.Title>
            <Dialog.Close />
          </Dialog.Header>
          <Dialog.Body>
            The overlay remains mounted for interaction but is visually hidden.
          </Dialog.Body>
        </Dialog.Panel>
      </Dialog>
    </div>
  );
}

render(<OverlayBehaviorDemo />);

Header Type​

Use headerType to switch the visual treatment of the dialog header without changing the rest of the panel structure.


function HeaderTypeDemo() {
  const headerTypes = ['basic', 'divided', 'light', 'dark', 'primary'];

  return (
    <div className="flex flex-wrap justify-center gap-3">
      {headerTypes.map((headerType) => (
        <Dialog key={headerType}>
          <Dialog.Trigger as={Button}>{headerType}</Dialog.Trigger>
          <Dialog.Overlay />
          <Dialog.Panel>
            <Dialog.Header headerType={headerType}>
              <Dialog.Title>{headerType} header</Dialog.Title>
              <Dialog.Close />
            </Dialog.Header>
            <Dialog.Body>
              <Dialog.Description>
                Header styling changes with <code>headerType</code>.
              </Dialog.Description>
              <div className="mt-3">
              This panel keeps the same body and footer while only the header
              appearance changes.
              </div>
            </Dialog.Body>
          </Dialog.Panel>
        </Dialog>
      ))}
    </div>
  );
}

render(<HeaderTypeDemo />);

Dismissible​

Set dismissible={false} when the dialog should stay open until the user explicitly closes it.


function DismissibleDemo() {
  return (
    <Dialog dismissible={false}>
      <Dialog.Trigger as={Button}>Open sticky dialog</Dialog.Trigger>
      <Dialog.Overlay />
      <Dialog.Panel>
        <Dialog.Header headerType="light">
          <Dialog.Title>Dismiss disabled</Dialog.Title>
          <Dialog.Close />
        </Dialog.Header>
        <Dialog.Body>
          Escape and outside clicks will not close this dialog. Use the close
          button to dismiss it.
        </Dialog.Body>
      </Dialog.Panel>
    </Dialog>
  );
}

render(<DismissibleDemo />);

Use footerType to switch the footer treatment without changing the action layout.


function FooterTypeDemo() {
  const footerTypes = ['basic', 'divided', 'light'];

  return (
    <div className="flex flex-wrap justify-center gap-3">
      {footerTypes.map((footerType) => (
        <Dialog key={footerType}>
          <Dialog.Trigger as={Button}>{footerType}</Dialog.Trigger>
          <Dialog.Overlay />
          <Dialog.Panel>
            <Dialog.Header headerType="divided">
              <Dialog.Title>{footerType} footer</Dialog.Title>
              <Dialog.Close />
            </Dialog.Header>
            <Dialog.Body>
              Footer styling changes with <code>footerType</code>.
            </Dialog.Body>
            <Dialog.Footer footerType={footerType}>
              <Button variant="secondary">Cancel</Button>
              <Button>Confirm</Button>
            </Dialog.Footer>
          </Dialog.Panel>
        </Dialog>
      ))}
    </div>
  );
}

render(<FooterTypeDemo />);

Controlled​

Use open and onOpenChange when the dialog state needs to be managed from outside the component.


function ControlledDemo() {
  const [open, setOpen] = useState(false);

  return (
    <div className="flex items-center gap-3">
      <Button onClick={() => setOpen(true)}>Open externally</Button>
      <span className="text-xs opacity-70">
        State: {open ? 'open' : 'closed'}
      </span>
      <Dialog open={open} onOpenChange={setOpen} dismissible={false}>
        <Dialog.Overlay />
        <Dialog.Panel>
          <Dialog.Header headerType="light">
            <Dialog.Title>Controlled dialog</Dialog.Title>
            <Dialog.Close />
          </Dialog.Header>
          <Dialog.Body>
            This dialog is controlled outside the component. Escape and outside
            clicks are disabled with <code>dismissible=false</code>.
          </Dialog.Body>
          <Dialog.Footer className="flex justify-end">
            <Dialog.Close as={Button}>Done</Dialog.Close>
          </Dialog.Footer>
        </Dialog.Panel>
      </Dialog>
    </div>
  );
}

render(<ControlledDemo />);

Accessibility & Keyboard​

  • The dialog renders with the proper role, aria-labelledby, and aria-describedby wiring through Spar.
  • Focus is trapped inside the content while open and restored on close.
  • Dialog.Title provides the accessible label and Dialog.Description provides the accessible description.
  • Dialog.Body provides the default content spacing area between header and footer.
  • When dismissible={false}, Escape and outside interaction do not close the dialog.
KeyBehavior
EscapeCloses the dialog unless dismissible={false}.
Tab / Shift+TabCycles focus within the dialog content.

API Reference​

Dialog​

Props​

NameTypeDefaultDescription
childrenReact.ReactNode-Dialog parts rendered inside the root.
forceMountbooleantrueKeep the overlay and panel mounted while closed so the exit transition can run. Pass false to unmount the panel on close. The wrapper flips Spar's default.
dismissiblebooleantrueWhether the dialog can be dismissed by clicking outside or pressing Escape.
idstring-Custom base ID for ARIA relationships. If not provided, one will be generated automatically. Sub-element IDs are derived as ${id}-title, ${id}-description, ${id}-content.
disabledbooleanfalseDisables all dialog triggers (prevents opening)
openboolean-Controlled open state
defaultOpenbooleanfalseInitial open state (uncontrolled)
modalbooleantrueWhether dialog is modal (blocks interaction outside)

Events​

NameTypeDefaultDescription
onOpenChange(open: boolean) => void-Callback when open state changes

Dialog.Trigger​

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Dialog.Overlay​

Props​

NameTypeDefaultDescription
invisiblebooleanfalseWhen true, the overlay is rendered but visually invisible.
intensityDialogOverlayIntensity'base'Overlay backdrop intensity.
blurbooleanfalseApplies backdrop blur on the overlay.
classNamesPartial<Record<"root", string>>-
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-
containerHTMLElement | nulldocument.bodyPortal container element. Content is portaled to document.body by default.

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-stateAlways"open" or "closed" — used for overlay fade transitions.
data-intensityAlwaysReflects the resolved intensity prop.
data-invisibleWhen invisible is trueIndicates the overlay is visually hidden but still mounted.
data-blurWhen blur is trueEnables backdrop blur styling on the overlay.

Dialog.Panel​

Props​

NameTypeDefaultDescription
classNamesPartial<Record<"root", string>>-
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-
roleReact.AriaRole'dialog'ARIA role for dialog type
containerHTMLElement | nulldocument.bodyPortal container element. Content is portaled to document.body by default.
trapFocusbooleantrueEnable focus trapping
restoreFocusbooleantrueRestore focus on close
initialFocusHTMLElement | (() => HTMLElement)-Element to focus on open
finalFocusHTMLElement | (() => HTMLElement)-Element to focus on close

Events​

NameTypeDefaultDescription
onOpenAutoFocus(event: Event) => void-Callback before auto-focus
onCloseAutoFocus(event: Event) => void-Callback before focus restore
onEscapeKeyDown(event: KeyboardEvent) => void-Escape key handler
onPointerDownOutside(event: PointerEvent) => void-Outside click handler
onInteractOutside(event: PointerEvent) => void-Outside interaction handler with preventDefault capability

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-stateAlways"open" or "closed" — forwarded from Spar dialog state.
data-modalAlwaysReflects whether the dialog is modal.
data-roleAlwaysReflects the resolved ARIA role.

Dialog.Header​

Props​

NameTypeDefaultDescription
headerTypeDialogHeaderType'basic'Type of the header.
classNamesPartial<Record<"root", string>>-
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-header-typeAlwaysReflects the resolved headerType prop.

Dialog.Title​

Props​

NameTypeDefaultDescription
classNamesPartial<Record<"root", string>>-
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-
levelnumber5Heading level (1-6)

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Dialog.Description​

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Dialog.Body​

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
NameTypeDefaultDescription
footerTypeDialogFooterType'basic'Type of the footer.
classNamesPartial<Record<"root", string>>-
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-
AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-footer-typeAlwaysReflects the resolved footerType prop.

Dialog.Close​

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Type Definitions​

NameDefinition
DialogOverlayIntensity'lightest' | 'light' | 'base' | 'dark' | 'darkest'
DialogHeaderType'basic' | 'divided' | 'light' | 'dark' | 'primary'
DialogFooterType'basic' | 'divided' | 'light'