Modal
A composable, accessible Modal component for modern React applications.
tsx
import { Modal } from '@nofinite/nui';Interactive Preview
A portal-based dialog primitive implementing focus isolation, background inerting, scroll locking, and controlled mounting with animated lifecycle transitions. Designed for transactional workflows, confirmations, forms, and interruptive UI.
Usage
Basic
tsx
import { Modal } from '@nofinite/nui';
<Modal open={open} onClose={() => setOpen(false)}>
Content
</Modal>With title + description
tsx
<Modal
open={open}
onClose={close}
title="Delete account"
description="This action cannot be undone."
>
<Actions />
</Modal>Form dialog with initial focus
tsx
<Modal
open={open}
onClose={close}
title="Edit profile"
initialFocusRef={inputRef}
>
<Form />
</Modal>Disable outside click
tsx
<Modal open={open} onClose={close} disableClickOutside>
Blocking flow
</Modal>Variants
Modal exposes lifecycle state variants via data attributes.
tsx
<Modal open />
<Modal disableEsc />
<Modal disableClickOutside />Available variants:
- closed
- mounted
- visible
- overlay-open
- dialog-open
Guidelines:
- Use for interruptive workflows only
- Avoid nested modals
Sizes
No intrinsic size API.
Default constraints:
max-width: 500pxmax-height: 90vh- Mobile safe margin
- Scrollable content region
Sizing should be overridden via className.
Shapes / Modes
Interaction modes
- ESC close
- Outside click close
- Explicit close button
- Controlled close only (disabled dismissal)
Accessibility modes
- Labelled dialog
- Described dialog
- Focus-trapped modal
- Background inert environment
Lifecycle modes
- Mounted
- Visible
- Animating in
- Animating out
- Unmounted
Accessibility
Implemented:
role="dialog"aria-modal="true"aria-labelledby/aria-describedby- Focus trap
- Initial focus management
- Focus restoration
- Background inerting
- Scroll locking
- ESC dismissal
- Overlay click dismissal
- Close button with accessible label
Strong compliance with WCAG modal dialog guidance.
Limitations:
- No alertdialog role variant
- No nested focus trap stacking
- No announcement for dynamic content updates
- No aria-live integration
Animation
Lifecycle animation model:
- Overlay fade
- Dialog scale + fade
- Mount delay for entrance
- Exit delay for unmount
- GPU-accelerated transform animation
- Respects reduced motion preference
Architectural notes
Key behaviors:
- Portal rendering avoids stacking context conflicts
- Two-phase state (mounted + visible) enables exit animation
- Focus trap lifecycle bound to visibility
- Inert background ensures screen reader isolation
- Click catcher prevents overlay bubbling conflicts
- Scroll lock prevents layout shift
- Ref preservation restores user context
- ID auto-generation ensures accessibility linkage
Tradeoffs:
- Fixed animation duration coupling with unmount timing
- No collision-aware viewport repositioning
- No scrollable overlay strategy for extremely tall dialogs
- Potential inert polyfill dependency in older browsers
Best practices
Do
- Use for blocking user decision flows
- Provide explicit close affordance
- Maintain concise content hierarchy
- Use initialFocusRef for form dialogs
- Override size for content-heavy dialogs
- Provide keyboard-accessible primary action
Don’t
- Nest modals
- Use for persistent UI
- Overload with navigation content
- Disable dismissal without strong UX reason
- Place interactive content outside focus trap
- Rely solely on overlay click for close
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
open* | boolean | — | — |
onClose* | () => void | — | — |
title | ReactNode | — | — |
description | ReactNode | — | — |
labelledById | string | — | — |
describedById | string | — | — |
disableClickOutside | boolean | false | — |
disableEsc | boolean | false | — |
initialFocusRef | RefObject<HTMLElement | null> | — | — |
overlayClassName | string | — | — |
hideCloseButton | boolean | false | Hides the 'X' close button in the top right corner. |