Components

Dialog

Headless modal dialog primitives with focus containment, dismissal, accessible naming, and compound state management.

When to Use

Use Dialog when a task or piece of information must appear above the page and the user should finish or dismiss it before returning. Use AlertDialog for a short, urgent decision such as confirming deletion. Use Drawer when the same modal behavior should be identified as a side sheet, and Popover for a small non-modal layer attached to a control.

Features

  • Supports controlled and uncontrolled open state with close reasons.
  • Traps focus while open, restores focus after close, and locks page scrolling.
  • Supports Escape and backdrop dismissal independently.
  • Closes nested dismissable layers before their parent dialog.
  • Registers portalled descendant layers in the dialog focus scope.
  • Supports keep-mounted content for consumer-owned exit animation.
  • Provides generated title and description relationships.

Import

import { Dialog } from "@flowstack-ui/atom";

Anatomy

<Dialog.Root>
  <Dialog.Trigger />
  <Dialog.Portal>
    <Dialog.Overlay />
    <Dialog.Content>
      <Dialog.Title />
      <Dialog.Description />
      <Dialog.Close />
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

API Reference

Root

Owns open state, generated IDs, dismissal options, and the trigger reference. It renders no wrapper element.

PropTypeDefault
openboolean-
defaultOpenbooleanfalse
onOpenChange(open: boolean, reason?: ModalCloseReason) => void-
closeOnEscapebooleantrue
closeOnBackdropClickbooleantrue
disabledbooleanfalse
keepMountedbooleanfalse

Close reasons include "backdropClick", "closeClick", and "escapeKeyDown" for Dialog interactions.

Trigger

Opens the dialog and receives the generated relationship to Content. It is a native button by default; custom elements receive button semantics.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
role"button" for custom elements
aria-haspopup"dialog"
aria-expandedCurrent open state
aria-controlsContent ID while open
aria-disabled"true" when Root is disabled
Data attributeValues
[data-slot]"dialog-trigger"
[data-state]"open" | "closed"
[data-disabled]Present when disabled

Portal

Moves its children to document.body by default without adding a wrapper.

PropTypeDefault
containerElement | DocumentFragment | nulldocument.body
disabledbooleanfalse

Overlay

Creates the backdrop and requests a backdropClick close when clicked. Its own disabled prop suppresses that request without disabling the whole Dialog.

PropTypeDefault
disabledbooleanfalse
ARIA attributeValues
aria-hidden"true"
Data attributeValues
[data-slot]"dialog-overlay"
[data-state]"open" | "closed"
[data-positioned]Present after the opening frames

Content

Renders the focus-trapped dialog panel as a div. Title and Description provide its generated accessible name and description unless ariaLabel supplies a fallback name.

PropTypeDefault
ariaLabelstring-
role"dialog" | "alertdialog""dialog"
ARIA attributeValues
roleValue from role
aria-modal"true" while visible
aria-labelValue from ariaLabel
aria-labelledbyGenerated Title ID when ariaLabel is absent
aria-describedbyGenerated Description ID
Data attributeValues
[data-slot]"dialog-content"
[data-state]"open" | "closed"
[data-positioned]Present after the opening frames

With keepMounted, closed Content remains inside a hidden, aria-hidden wrapper and does not expose aria-modal.

Title

Supplies the heading referenced by Content. It renders an h2 by default and accepts native heading props.

PropTypeDefault
as"h1" | "h2" | "h3" | "h4" | "h5" | "h6""h2"
Data attributeValues
[data-slot]"dialog-title"

Description

Supplies the explanatory text referenced by Content. It renders a p and accepts native paragraph props.

Data attributeValues
[data-slot]"dialog-description"

Close

Closes Root with reason "closeClick". It renders a native button by default and supports custom composition.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"dialog-close"

The Dialog entry point also exports useModalContext and useModalContent for advanced custom modal parts. Prefer the namespaced parts above for ordinary dialogs because they supply the complete focus and ARIA contract.

Examples

Edit Profile

import { Dialog } from "@flowstack-ui/atom";

export function EditProfileDialog() {
  return (
    <Dialog.Root>
      <Dialog.Trigger>Edit profile</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay />
        <Dialog.Content>
          <Dialog.Title>Edit profile</Dialog.Title>
          <Dialog.Description>Update your public account details.</Dialog.Description>
          <label>
            Display name
            <input name="displayName" />
          </label>
          <Dialog.Close>Done</Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

Controlled Close Reasons

import { useState } from "react";
import { Dialog, type ModalCloseReason } from "@flowstack-ui/atom";

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

  function handleOpenChange(nextOpen: boolean, reason?: ModalCloseReason) {
    setOpen(nextOpen);
    if (!nextOpen && reason) console.log(`Closed by ${reason}`);
  }

  return (
    <Dialog.Root open={open} onOpenChange={handleOpenChange}>
      <Dialog.Trigger>Show details</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay />
        <Dialog.Content>
          <Dialog.Title>Account details</Dialog.Title>
          <Dialog.Description>Your current account information.</Dialog.Description>
          <Dialog.Close>Close</Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

Accessibility

Dialog follows the WAI-ARIA Modal Dialog pattern. Always provide a clear Title or ariaLabel; include Description when users need context before acting. Focus moves inside when opened, remains within the dialog and its registered descendant portals, then returns to the prior focus target after close.

KeyDescription
EscapeCloses the topmost dialog when closeOnEscape is enabled.
TabMoves to the next focusable element in the dialog scope.
Shift+TabMoves to the previous focusable element in the dialog scope.
Enter / SpaceOpens from Trigger and activates native button controls.

Changelog

See CHANGELOG.md.