Skip to main content
Components

React Modal Components

Accessible modal dialogs with focus trapping, scroll lock, and portal rendering. Built on Radix UI — no accessibility bugs to fix.

import { Modal } from "@ninna-ui/overlays";

<Modal>
  <Modal.Trigger>Open</Modal.Trigger>
  <Modal.Content>
    <Modal.Header>Title</Modal.Header>
    <Modal.Body>Content here</Modal.Body>
    <Modal.Footer>
      <Modal.Close>Close</Modal.Close>
    </Modal.Footer>
  </Modal.Content>
</Modal>

Key features

Focus trapping — tab stays inside the modal

Scroll lock — background doesn't scroll

Portal rendering — escapes z-index stacking contexts

Compound API: Modal.Trigger, Modal.Content, Modal.Close

Keyboard: Esc closes, Tab cycles focus

data-slot CSS targets for every modal internal

Accessible by default

Modals are one of the hardest components to make accessible. Focus must be trapped inside the dialog, the background must stop scrolling, Esc must close, and screen readers must announce the dialog title. Ninna UI's Modal handles all of this via Radix UI primitives.

You compose the modal using compound components: Modal.Trigger opens the dialog, Modal.Content renders the portal, Modal.Header/Body/Footer structure the content, and Modal.Close dismisses it.

Compound component API

The compound API keeps props close to where they're used. No giant prop list on the root component:

<Modal>
  <Modal.Trigger asChild>
    <Button color="primary">Delete account</Button>
  </Modal.Trigger>
  <Modal.Content className="max-w-md">
    <Modal.Header>
      <Modal.Title>Delete account?</Modal.Title>
      <Modal.Description>This action cannot be undone.</Modal.Description>
    </Modal.Header>
    <Modal.Body>
      <Text>Are you sure you want to delete your account?</Text>
    </Modal.Body>
    <Modal.Footer>
      <Modal.Close asChild>
        <Button variant="ghost">Cancel</Button>
      </Modal.Close>
      <Button color="danger">Delete</Button>
    </Modal.Footer>
  </Modal.Content>
</Modal>

Component Docs

FAQ