Modal
A portal-based, fully accessible dialog primitive engineered for interruptive workflows, confirmations, and multi-step forms. Implements WAI-ARIA 1.2 modal dialog standards, focus trapping, background inerting, body scroll locking, and physics-based entrance/exit transitions.
import { Modal } from '@nofinite/nui';Interactive Preview
Anatomy & Modes
NUI Modal can be used as a self-contained single element via declarative props (matching Storybook's default pattern) or composed via structured compound components for custom layout arrangements:
Declarative Props Mode (Recommended)
<Modal
open={open}
onClose={handleClose}
title="Modal Title"
description="Descriptive helper subtitle."
footer={
<>
<Button variant="outline" onClick={handleClose}>Cancel</Button>
<Button variant="primary" onClick={handleConfirm}>Confirm</Button>
</>
}
>
<p>Modal body content goes here.</p>
</Modal>Compound Components Mode
<Modal open={open} onClose={handleClose}>
<Modal.Header>
<Modal.Title>Modal Title</Modal.Title>
<Modal.Description>Descriptive helper subtitle.</Modal.Description>
</Modal.Header>
<Modal.Body>
{/* Scrollable content area */}
</Modal.Body>
<Modal.Footer>
{/* Action buttons */}
</Modal.Footer>
</Modal>| Component / Prop | Description |
|---|---|
Modal | The root portal controller and backdrop overlay container. |
title | Accessible title prop rendered into h2.modal-title. |
description | Accessible description subtitle rendered into p.modal-description. |
footer | Custom footer actions container rendered with border separation and flex-end alignment. |
Modal.Header | Container for the dialog title and descriptive subtitle. |
Modal.Title | Accessible heading (<h2>) linked via aria-labelledby. |
Modal.Description | Accessible subtitle (<p>) linked via aria-describedby. |
Modal.Body | Main scrollable content region with responsive padding. |
Modal.Footer | Bottom action bar with border separation and flex-end alignment. |
Usage Examples
1. Standard Declarative Modal
Self-contained modal using title, description, and footer props:
import React, { useState } from 'react';
import { Button, Modal } from '@nofinite/nui';
export default function DeploymentSettings() {
const [open, setOpen] = useState(false);
return (
<>
<Button variant="primary" onClick={() => setOpen(true)}>
Deployment Settings
</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
title="Production Deployment"
description="Configure edge nodes and SSL certificate options."
footer={
<>
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="primary" onClick={() => setOpen(false)}>Deploy Now</Button>
</>
}
>
<p className="text-sm text-muted leading-relaxed">
Your application will be distributed across 35 edge regions with automated fallback routing and instant cache invalidation.
</p>
</Modal>
</>
);
}2. Form Dialog with Initial Focus
Automatically focus an input field when the dialog opens using initialFocusRef:
import React, { useState, useRef } from 'react';
import { Button, Modal, Input } from '@nofinite/nui';
export default function RenameProjectModal() {
const [open, setOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<Button variant="outline" onClick={() => setOpen(true)}>Rename Project</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
initialFocusRef={inputRef}
>
<Modal.Header>
<Modal.Title>Rename Project</Modal.Title>
<Modal.Description>Choose a unique identifier for your workspace.</Modal.Description>
</Modal.Header>
<Modal.Body>
<div className="space-y-4">
<label className="block text-sm font-medium text-default">Project Name</label>
<Input ref={inputRef} defaultValue="nebula-ui" placeholder="e.g. nebula-ui" />
</div>
</Modal.Body>
<Modal.Footer>
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="primary" onClick={() => setOpen(false)}>Save Name</Button>
</Modal.Footer>
</Modal>
</>
);
}3. Non-Dismissible Transactional Modal
Prevent accidental dismissal during critical or destructive workflows:
<Modal
open={isMigrating}
onClose={() => {}}
disableClickOutside
disableEsc
hideCloseButton
>
<Modal.Header>
<Modal.Title>Migrating Database</Modal.Title>
</Modal.Header>
<Modal.Body>
<p className="text-sm text-muted">Please do not close this window while the migration executes.</p>
</Modal.Body>
</Modal>Keyboard Interaction
| Key | Behavior |
|---|---|
| Escape | Closes the modal and restores focus to the previously focused trigger element (unless disableEsc is active). |
| Tab | Traps and cycles focus forward through all interactive elements within the modal dialog. |
| Shift + Tab | Cycles focus backwards within the modal focus trap. |
Accessibility (WAI-ARIA)
- Role & Semantics: Root dialog renders
role="dialog"andaria-modal="true". - Labeling: Automatically links
aria-labelledbytoModal.Titleandaria-describedbytoModal.Description. - Focus Management: Focus is automatically trapped inside the dialog upon mounting and returned to the trigger element on close.
- Background Inerting: All sibling nodes outside the portal are marked
inertto prevent screen reader bleed. - Scroll Locking: Automatically locks body scroll with padding compensation to avoid layout shift.
Design Tokens & NUICSS Superclasses
NUI Modal strictly adheres to semantic NUICSS tokens and superclasses:
| Superclass / Token | Type | Usage |
|---|---|---|
modal-box | Superclass | Dialog container surface with elevation, radius, and max-width. |
modal-header | Superclass | Top header area with title spacing. |
modal-title | Superclass | Semantically styled dialog title. |
modal-description | Superclass | Muted subtitle text. |
modal-body | Superclass | Scrollable body area with responsive gutters. |
modal-footer | Superclass | Bottom border-separated action bar. |
modal-close | Superclass | Absolute close button in the upper-right corner. |
bg-overlay | Token | Semi-transparent dimmed backdrop (--bg-overlay). |
bg-surface | Token | Dialog panel background surface (--bg-surface). |
border-default | Token | Container border token (--border-default). |
Best Practices
Do
- Always provide clear
Modal.TitleandModal.Descriptionfor screen reader context. - Use
initialFocusReffor form dialogs to guide keyboard users directly to the first input. - Provide explicit action buttons in
Modal.Footerto clearly communicate outcomes.
Don't
- Do not nest modals inside other modals; use sequential drawers or full-page workflows instead.
- Avoid using modals for non-interruptive information that could live directly on the page canvas.
- Do not disable outside click or Escape dismissal without an explicit functional reason (such as unsaved changes).
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
open* | boolean | — | — |
onClose* | () => void | — | — |
title | ReactNode | — | — |
description | ReactNode | — | — |
footer | ReactNode | — | Custom footer actions (rendered with modal-footer styling and border separation) |
labelledById | string | — | — |
describedById | string | — | — |
disableClickOutside | boolean | — | — |
disableEsc | boolean | — | — |
initialFocusRef | RefObject<HTMLElement | null> | — | — |
overlayClassName | string | — | — |
hideCloseButton | boolean | — | Hides the 'X' close button in the top right corner. |