Skip to main content
Markdown

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.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.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.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.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.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.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.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.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.
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)
forceMountbooleanfalseAlways render portal/overlay/content (for animation libraries)
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'