apx-dsv0.1Local renderer · live
K
Foundations4
  • Getting started
  • Theming
  • Templates
  • Icons
  • Avatar
  • Badge
  • DataGrid
  • Scheduler
  • Stat
  • Table
  • Timeline
  • TreeView
  • Accordion
  • Alert
  • EmptyState
  • Progress
  • Skeleton
  • Spinner
  • SplashScreen
  • Toast
  • ColorPicker
  • FileUpload
  • Form
  • Rating
  • TagsInput
  • Combobox
  • Field
  • Select
  • Toggle
  • Button
  • Calendar
  • Checkbox
  • DatePicker
  • Input
  • NumberInput
  • Radio
  • Slider
  • Switch
  • Textarea
  • AppShell
  • Div
  • Divider
  • Sidebar
  • Stack
  • Typography
  • Image
  • Breadcrumbs
  • Carousel
  • NavigationMenu
  • Pagination
  • Stepper
  • Tabs
  • Toolbar
  • CommandPalette
  • Confirm
  • Drawer
  • HoverCard
  • Menu
  • Modal
  • Popover
  • Tooltip
  • Icon
  • Card
  • PricingCard
60 componentsapx-ds/renderer
Modal
Variant↳ other

Overlays

Modal

Blocking dialog overlay. Compound API (

Overview

<Modal /> is the canonical blocking dialog primitive. Where <Popover /> is a non-blocking floating panel anchored to a trigger, Modal is a viewport-centered surface that:

  • Captures all pointer events behind a backdrop.
  • Traps keyboard focus inside its Content.
  • Locks page scroll while open (no layout shift; iOS-aware).
  • Carries role="dialog" + aria-modal="true".
  • Composes a structured Header / Body / Footer compound for the common confirm / form / content-viewer shapes.

Modal is also the first consumer of useScrollLock, closing the Phase 17 Core overlay primitive audit (usePosition / <Portal> / useEscapeStack / useFocusTrap / useOutsideClick / useScrollLock are now all consumer-validated).

Overview — confirmation dialog with header, body, and footer

Loading preview…
Overview.tsx

When to use

  • A blocking confirmation: "Delete account?".
  • A short form whose work shouldn't be lost to accidental navigation.
  • A viewer for content that needs the full visual focus (image lightbox, large editor).
  • A wizard step that is meaningfully separate from the surrounding page.

When NOT to use

  • The content is purely a hint with no interactivity → <Tooltip />.
  • The content is a small interactive panel anchored to a trigger → <Popover />.
  • The content is a page-level workflow that the user needs to navigate while still seeing the page → <Drawer /> (Phase 20, builds on the same backdrop + scroll-lock pattern).
  • The content is a list of actions on a button → <Menu /> (Phase 22).
  • The content is a transient acknowledgement → <Toast /> (Phase 21).

Anatomy

text
<Modal>
  <Modal.Trigger> button </Modal.Trigger>
  <Modal.Content>
    <Modal.Close />               (optional × button in the top-right corner)
    <Modal.Header
      title="…"                   (wired into aria-labelledby)
      description="…"             (wired into aria-describedby)
      avatar={…}                  (optional leading slot)
      action={…}                  (optional trailing slot)
    />
    <Modal.Body>… scrollable region …</Modal.Body>
    <Modal.Footer align="end">
      <Modal.Close asChild><Button variant="ghost">Cancel</Button></Modal.Close>
      <Button>Save</Button>
    </Modal.Footer>
  </Modal.Content>
</Modal>
<Modal>
  <Modal.Trigger> button </Modal.Trigger>
  <Modal.Content>
    <Modal.Close />               (optional × button in the top-right corner)
    <Modal.Header
      title="…"                   (wired into aria-labelledby)
      description="…"             (wired into aria-describedby)
      avatar={…}                  (optional leading slot)
      action={…}                  (optional trailing slot)
    />
    <Modal.Body>… scrollable region …</Modal.Body>
    <Modal.Footer align="end">
      <Modal.Close asChild><Button variant="ghost">Cancel</Button></Modal.Close>
      <Button>Save</Button>
    </Modal.Footer>
  </Modal.Content>
</Modal>
  • Modal — context provider. Owns open, the trigger / content refs, the lifecycle hooks (useEscapeStack, useScrollLock), and the ARIA ids (triggerId / titleId / descId).
  • Modal.Trigger — clones a single child (asChild, default), attaches click + ARIA. Or renders an inline <button> when asChild={false}. Optional — controlled Modals can omit it.
  • Modal.Content — the portal-rendered dialog surface. Carries the visual axes (variant / size / placement / overlay).
  • Modal.Header — title + description + optional avatar + action slots. Auto-wires ARIA via the parent context.
  • Modal.Body — scrollable middle region (flex-1 overflow-y-auto).
  • Modal.Footer — button-row with align variant (start / center / end / between).
  • Modal.Close — built-in × button (asChild-able) that calls setOpen(false).

Variants

Two variants of the Content surface — Modal is a structural primitive, not a stylistic one:

  • solid — paper background, shadow-2xl, transparent border. Default.
  • outline — paper background, 1px subtle border, shadow-xl. Quieter; pairs well with heavy blur backdrops.

Sizes

SizeMax-widthPad (sm/md/lg/xl)Notes
smmax-w-smp-4Confirms, simple prompts
mdmax-w-mdp-5Default — short forms
lgmax-w-lgp-6Multi-section dialogs
xlmax-w-2xlp-6Editors, content viewers
fullviewport-edgesp-6Fullscreen takeover (lightboxes)
fitmax-w-fitp-5Content-driven sizing

Placement & overlay

PropValuesDefault
placement'center' (vertical center) / 'top' (anchored 10vh from top)'center'
overlay'dimmed' / 'blur' / 'transparent''dimmed'

Behavior

PropDefaultEffect
closeOnEscapetrueEsc closes the topmost Modal (escape-stack ordering).
closeOnBackdropClicktrueClick on the backdrop (not inside Content) closes the Modal.
trapFocustrueFocus moves into Content on open; Tab cycles inside; focus returns to trigger on close.
preventScrolltrueLocks document.body scroll while open via the engine's reference-counted useScrollLock.
initialFocus(first focusable)Element to focus on open. Defaults to the first focusable inside Content; falls back to Content itself (tabIndex={-1}).
finalFocus(the trigger)Element to focus on close. Defaults to whatever opened the Modal.

Accessibility

  • Trigger carries aria-haspopup="dialog", aria-expanded, data-state="open"|"closed".
  • Content carries role="dialog", aria-modal="true", aria-labelledby={titleId}, aria-describedby={descId} — both ids resolve to nodes rendered by <Modal.Header>'s title and description props automatically.
  • Focus management (when trapFocus={true}):
    • On open: focus moves to initialFocus if provided, else first focusable child, else Content itself (which carries tabIndex={-1}).
    • Tab cycles inside Content via the engine's useFocusTrap.
    • On close: focus returns to the trigger (or finalFocus if provided).
  • Escape closes via the engine's useEscapeStack — only the topmost Modal closes per press, so nested Modals unwind cleanly (see <NestedModal /> example).
  • Body scroll is locked via useScrollLock while open, restored on close. Reference-counted, so a Modal-over-Drawer combo collapses into one lock + unlock pair.
  • Backdrop click uses an e.target === e.currentTarget sentinel — clicks inside Content (inputs, buttons) bubble up but never satisfy the sentinel, so they never close the Modal.
  • Close button carries aria-label="Close" by default; override via aria-label prop.
  • axe-core: zero violations across the full variant × size × open-state matrix.

Examples

Overview

Loading preview…
Overview.tsx

Basic

Loading preview…
Basic.tsx

Sizes

Loading preview…
Sizes.tsx

Placements

Loading preview…
Placements.tsx

Overlays

Loading preview…
Overlays.tsx

ConfirmDelete

Loading preview…
ConfirmDelete.tsx

ScrollableBody

Loading preview…
ScrollableBody.tsx

FormInside

Loading preview…
FormInside.tsx

NestedModal

Loading preview…
NestedModal.tsx

Controlled

Loading preview…
Controlled.tsx

Programmatic

Loading preview…
Programmatic.tsx

WithoutTrigger

Loading preview…
WithoutTrigger.tsx

Theming

tsx
defineTheme({
  components: {
    Modal: {
      defaultProps: { /* root behavior props — closeOnEscape / preventScroll / etc. */ },
      styleOverrides: {
        backdrop: 'bg-fg-default/50',
        content: 'shadow-2xl',
        header: '',
        body: '',
        footer: '',
        close: 'text-fg-muted',
      },
    },
  },
});
defineTheme({
  components: {
    Modal: {
      defaultProps: { /* root behavior props — closeOnEscape / preventScroll / etc. */ },
      styleOverrides: {
        backdrop: 'bg-fg-default/50',
        content: 'shadow-2xl',
        header: '',
        body: '',
        footer: '',
        close: 'text-fg-muted',
      },
    },
  },
});

Per-instance overrides via <Modal.Content className sx style /> (and the equivalent on every subpart) merge on top of the theme overrides, which merge on top of the recipe — the same precedence the rest of the DS uses, re-validated for compound components by Core 18's defaultProps wiring.

Props

PropTypeDefaultDescription
closeOnBackdropClickboolean—Default: `true`. Clicking the backdrop closes the Modal. Set to `false` for destructive- confirm modals where an accidental backdrop click should not lose work.
closeOnEscapeboolean—Default: `true`. Esc closes the topmost Modal (escape-stack ordering).
defaultOpenboolean—Initial `open` for the uncontrolled case. Default: `false`.
finalFocusRefObject<HTMLElement | null>—Element to focus when Content unmounts. Defaults to the trigger that opened the Modal (if there was one); falls back to whatever was focused at open-time.
initialFocusRefObject<HTMLElement | null>—Element to focus when Content mounts. Defaults to the first focusable descendant; if there are none, Content itself (carries `tabIndex={-1}`) is focused.
onOpenChange(open: boolean) => void—Notified whenever `open` changes (controlled or uncontrolled).
openboolean—Controlled `open`. When omitted, Modal manages its own state via `defaultOpen`.
preventScrollboolean—Default: `true`. Locks `document.body` scroll while open via the engine's reference-counted `useScrollLock` (so a Modal-over-Drawer combo collapses into one lock + unlock pair).
trapFocusboolean—Default: `true`. Focus is trapped inside Content while open and restored on close.