Modal
A fully accessible, WCAG-compliant modal/dialog component for blocking user interaction and capturing focused attention during critical workflows.
Usage
Basic usage
'use client';
import { useState } from 'react';
import { Modal } from '@nofinite/nui';
const Page = () => {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Modal open={open} onClose={() => setOpen(false)} title="Confirm action">
<p>Are you sure you want to continue?</p>
</Modal>
</>
);
};
With confirmation actions
<Modal open={open} onClose={() => setOpen(false)} title="Confirm action">
<p>Are you sure you want to continue?</p>
<div style={{ marginTop: 16 }}>
<button onClick={() => setOpen(false)}>Cancel</button>
<button style={{ marginLeft: 8 }}>Confirm</button>
</div>
</Modal>
Variants
This Modal component does not expose visual variants by design.
Guidelines:
- Keep modal appearance consistent across the app.
- Customize visuals using
classNameor theme tokens instead of variants. - For destructive or special flows, adjust content—not the modal shell.
Sizes
The modal has a responsive, content-driven size:
- Width:
min(90vw, 720px) - Height: up to
90vhwith internal scrolling
Guidelines:
- Avoid forcing sizes unless absolutely necessary.
- Keep modal content concise and task-focused.
States
The modal supports several behavioral states:
<Modal open />
<Modal open disableEsc />
<Modal open disableClickOutside />
State behavior:
-
open- Controls visibility.
- When
false, the modal is not rendered.
-
disableEsc- Prevents closing the modal via
Esckey.
- Prevents closing the modal via
-
disableClickOutside- Prevents closing the modal by clicking outside the dialog.
Native Props / Composition
The modal supports composition via children and standard React props.
<Modal open={open} onClose={handleClose} className="custom-modal">
Custom modal content
</Modal>
classNameis applied to the dialog container.- Native focusable elements inside the modal work automatically.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controls whether the modal is visible |
onClose | () => void | — | Callback fired when the modal requests to close |
title | string | — | Modal title, used for accessible labeling |
description | string | — | Optional description for screen readers |
labelledById | string | — | Custom aria-labelledby ID |
describedById | string | — | Custom aria-describedby ID |
disableClickOutside | boolean | false | Disables click-outside-to-close behavior |
disableEsc | boolean | false | Disables closing the modal with Esc |
initialFocusRef | React.RefObject<HTMLElement> | — | Element to receive focus on open |
className | string | "" | Additional class names for styling |
children | React.ReactNode | — | Modal content |
Behavior Notes
-
The modal renders inside a portal attached to
<body>. -
Background content is made inert while the modal is open.
-
Focus is:
- Trapped inside the modal
- Restored to the previously focused element on close
-
Page scroll is locked while the modal is open.
-
Animations respect
prefers-reduced-motion.
Example:
<Modal open={open} onClose={handleClose} initialFocusRef={confirmButtonRef} />
Accessibility
-
Renders as:
<div role="dialog"> -
Uses:
aria-modal="true"aria-labelledbyaria-describedby
-
Keyboard support:
Tab/Shift+Tabfor focus trappingEscto close (unless disabled)
-
Background content is fully inert (not focusable or interactive)
-
Includes a screen-reader friendly close button
Example:
<Modal title="Delete item" description="This action cannot be undone" />
Layout
- Centered in the viewport
- Scrolls internally if content exceeds height
- Overlay blocks interaction with the app
<Modal open style={{ maxWidth: 480 }}>
Compact modal content
</Modal>
Best Practices
Do
- Use for critical decisions or blocking workflows
- Keep content concise and action-oriented
- Always provide a clear way to close the modal
- Use
titlefor accessible labeling
Don’t
- Nest modals inside modals
- Use for non-blocking or lightweight UI
- Disable
Escor outside click without strong reason - Overload the modal with complex layouts