|Kit

Modal

Centered overlay dialog on Ark UI's Dialog machine — focus trap, scroll lock, RTL and animated enter/exit, with a flat props API and composable parts.

The Modal is a centered overlay built on Ark UI's Dialog state machine. Ark owns focus-trapping, body scroll-lock, RTL, the Esc / click-outside behaviour and the data-state attribute that drives the enter/exit animations; the kit adds a flat props API plus styled, composable parts.

import { Modal } from '@42-central/ui-react/modal';

<Modal open={open} onOpenChange={setOpen} title="Welcome">
  <Text>Anything you like in the body.</Text>
</Modal>

To spawn modals imperatively — without useState at every call site — use the overlays manager.

Basic usage

open / onOpenChange make the modal a controlled component. onOpenChange is flattened to (open: boolean) => void.

Code

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

<Button onClick={() => setOpen(true)}>Open modal</Button>
<Modal open={open} onOpenChange={setOpen} title="Welcome aboard">
  <Text>Focus is trapped and the page is scroll-locked while open.</Text>
</Modal>

Sizes

size maps to the kit's width tokens (xs–xl), plus full for a near-fullscreen sheet.

Code

<Modal size="sm" … />
<Modal size="md" … />  // default
<Modal size="lg" … />
<Modal size="xl" … />

Centered

By default the modal anchors near the top of the viewport (so tall content grows downward). Set centered to vertically center it.

Code

<Modal centered title="Centered" … />

Description & close button

title and description render the header; withCloseButton (default true) adds a dogfooded ActionIcon close button.

Code

<Modal
  title="Delete file"
  description="This action cannot be undone."
  withCloseButton={false}
/>

Composable parts

The high-level <Modal> covers the common case. For full control over structure, compose the parts directly — they carry the same styling and Ark wiring:

<Modal.Root open={open} onOpenChange={({ open }) => setOpen(open)}>
  <Modal.Trigger asChild>
    <Button>Open</Button>
  </Modal.Trigger>
  <Modal.Backdrop />
  <Modal.Positioner centered>
    <Modal.Content size="md">
      <Modal.Header>
        <Modal.Title>Custom layout</Modal.Title>
        <Modal.CloseTrigger />
      </Modal.Header>
      <Modal.Body>…</Modal.Body>
      <Modal.Footer>
        <Button>Cancel</Button>
        <Button>Save</Button>
      </Modal.Footer>
    </Modal.Content>
  </Modal.Positioner>
</Modal.Root>

Modal.Root, Modal.Trigger, and Modal.Context are raw Ark re-exports with no DOM node (or styling) of their own, so none take a testId. Modal.Backdrop, Modal.Positioner, Modal.Content, Modal.Title, Modal.Description, Modal.Header, Modal.Body, and Modal.Footer each take their own testId, landing on their own rendered element. Modal.CloseTrigger's testId routes to the actual rendered close button — an inner ActionIcon, since CloseTrigger itself is asChild over it. See Testing.

API

Prop

Type

All other props (modal, trapFocus, preventScroll, id, lazyMount, …) forward to Ark's Dialog.Root.

Accessibility

  • Ark traps focus inside the content while open and restores it to the trigger on close.
  • The body is scroll-locked (preventScroll) and content below is hidden from assistive tech (modal).
  • title wires aria-labelledby and description wires aria-describedby automatically. When you omit a title, pass an aria-label.
  • Animations collapse to a 1ms opacity tween under prefers-reduced-motion.

Changelog

v0.19.0
  • A translations prop for the close, dismiss, collapse/expand and region labels; <Notifications /> forwards its keys to every toast

    #331
v0.18.0
  • asChild is a compile error on Combobox.Control, Group, Content, Panel and Item, Drawer/Modal.Positioner, Drawer.Grabber, Popover.Content and Tooltip.Arrow

    #294
  • CloseTrigger asChild uses your element in place of the icon button, without the forced aria-label

    #288
v0.16.0
  • An outside click no longer dismisses the parent while a nested overlay is open

    #251
v0.9.0
  • --modal-z rises from 50 to 150, leaving room for the toast band below it

    #135
v0.4.0
  • Floating surfaces take a brand tint

    #41
v0.3.0
  • Panels and highlights use translucent washes instead of opaque grays

    #35
v0.0.1
  • Modal and Drawer, controlled by open/onOpenChange; Drawer slides in from any edge

On this page