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>