Components

Modal

Shared foundation for modal dialog behavior, focus management, portals, titles, descriptions, and close controls.

When to Use

Use Modal when building another primitive that must keep attention inside an overlay until it closes. Most applications should use Dialog, AlertDialog, or Drawer instead because those components already provide a complete content surface. Use Popover when people must still interact with the page behind the floating content.

Features

  • Controlled and uncontrolled open state.
  • Escape-key and backdrop dismissal controls.
  • Stack-aware Escape dismissal so nested overlays close before the parent modal.
  • Focus trap, focus restore, scroll lock, and initial focus management.
  • Optional keep-mounted content support through useModalContent.
  • Close reasons for action, cancel, backdrop, Escape, and close-button flows.
  • Headless only: no styles, classes, icons, or animation opinions.

Import

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

Anatomy

<Modal.Root>
  <Modal.Trigger />
  <Modal.Portal>
    <Modal.Title />
    <Modal.Description />
    <Modal.Close />
  </Modal.Portal>
</Modal.Root>

useModalContent()

API Reference

Root

Provides modal state and shared IDs to compound parts.

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

Trigger

Renders the control that opens the modal and connects it to the generated modal content ID.

PropTypeDefault
childrenReactNoderequired
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-haspopup"dialog"
aria-expandedModal open state
aria-controlsModal ID while open
aria-disabledPresent when the root is disabled
Data attributeValues
[data-slot]"modal-trigger"
[data-state]"open" | "closed"
[data-disabled]Present when disabled

Portal

Moves modal content to document.body or a supplied container. Set disabled to leave the children in their original DOM location.

PropTypeDefault
childrenReactNoderequired
containerHTMLElement | nulldocument.body
disabledbooleanfalse

Title

Provides the accessible title referenced by modal content.

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

Description

Provides the accessible description referenced by modal content.

PropTypeDefault
childrenReactNoderequired
Data attributeValues
[data-slot]"modal-description"

Close

Closes the modal with reason: "closeClick".

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

useModalContent

Builds the modal content wrapper used by higher-level primitives. It supplies presence state, dialog ARIA relationships, focus containment, scroll locking, Escape dismissal, and a focus scope for descendant portals.

OptionTypeDefault
role"dialog" | "alertdialog""dialog"
ariaLabelstring-

Important return values include isPresent, isHidden, isPositioned, dataState, presenceRef, contentProps, focusScope, onClose, and closeOnBackdropClick. Spread contentProps onto the element receiving presenceRef; do not invent separate IDs or dialog relationships.

Examples

Custom Modal Foundation

import { Modal, useModalContent } from "@flowstack-ui/atom";

function ModalContent() {
  const modal = useModalContent();

  if (!modal.isPresent) return null;

  return (
    <Modal.Portal>
      <div
        {...modal.contentProps}
        ref={modal.presenceRef}
        hidden={modal.isHidden}
        data-state={modal.dataState}
        data-positioned={modal.isPositioned ? "" : undefined}
      >
        <Modal.Title>Settings</Modal.Title>
        <Modal.Description>Change account preferences.</Modal.Description>
        <Modal.Close>Close</Modal.Close>
      </div>
    </Modal.Portal>
  );
}

export function SettingsModal() {
  return (
    <Modal.Root>
      <Modal.Trigger>Open settings</Modal.Trigger>
      <ModalContent />
    </Modal.Root>
  );
}

onOpenChange receives a ModalCloseReason when the modal closes, allowing a higher-level primitive to distinguish Escape, backdrop, action, cancel, and close-control dismissal.

Accessibility

Modal supplies the foundation for the WAI-ARIA modal dialog pattern. It is normally consumed through Dialog, AlertDialog, or Drawer content parts.

  • Focus is trapped while open and restored when the modal closes.
  • Focus containment includes registered portalled layers owned by descendants, such as Select, Menu, and Popover content opened from inside the modal.
  • Provide a visible title and description when possible. Use an accessible label on the content wrapper when a visible title is not appropriate.
KeyDescription
EscapeCloses the modal when closeOnEscape is enabled.
TabMoves focus to the next focusable element inside the modal.
Shift+TabMoves focus to the previous focusable element inside the modal.

Changelog

See CHANGELOG.md.