Headless React modal dialog primitives with focus trapping, Escape handling, backdrop dismissal, scroll locking, ARIA wiring, and focus restoration.
The package ships no CSS. Consumers own layout, colors, animation, and spacing.
Clone and build the source release (Node.js 20 or later):
git clone https://github.com/jare20895/accessible-modal-react
cd accessible-modal-react
npm ci
npm run typecheck
npm test
npm run build
npm packTo use the built package, run npm install /absolute/path/to/accessible-modal-react/accessible-modal-react-0.1.0.tgz in your React app. These instructions use the GitHub source release; npm registry publication is not part of this release.
React and React DOM are peer dependencies. The release tests use React 18.3.1.
import { useRef } from "react";
import { Modal, useModal } from "accessible-modal-react";
export function PublishDialog() {
const triggerRef = useRef<HTMLButtonElement>(null);
const modal = useModal();
return (
<>
<button ref={triggerRef} onClick={modal.open}>
Publish
</button>
<Modal
isOpen={modal.isOpen}
onClose={modal.close}
triggerRef={triggerRef}
aria-labelledby="publish-title"
aria-describedby="publish-description"
>
{({ close }) => (
<div className="dialog-panel">
<h2 id="publish-title">Publish whitepaper</h2>
<p id="publish-description">This will make the current draft public.</p>
<button onClick={close}>Cancel</button>
<button onClick={close}>Publish</button>
</div>
)}
</Modal>
</>
);
}| Prop | Type | Default | Notes |
|---|---|---|---|
isOpen |
boolean |
required | Controls whether the modal renders. |
onClose |
() => void |
required | Called by Escape key, backdrop click, or render-prop close helpers. |
children |
ReactNode or render prop |
required | Render prop receives { close }. |
triggerRef |
RefObject<HTMLElement> |
current active element | Focus target after close. |
initialFocusRef |
RefObject<HTMLElement> |
first focusable element | Initial focus target after open. |
closeOnEscape |
boolean |
true |
Enables Escape key dismissal. |
closeOnBackdropClick |
boolean |
true |
Enables backdrop dismissal. |
onOpen |
() => void |
none | Called when the modal opens. |
aria-label |
string |
none | Use when no visible heading labels the dialog. |
aria-labelledby |
string |
none | ID of the visible dialog title. |
aria-describedby |
string |
none | ID of the dialog description. |
- Renders with
role="dialog"andaria-modal="true". - Supports
aria-label,aria-labelledby, andaria-describedby. - Moves focus into the dialog on open.
- Traps
TabandShift+Tabwithin the dialog. - Restores focus to
triggerRefor the previously active element after close. - Locks body scroll while open.
- Applies
aria-hidden="true"to body siblings outside the portal while open. - Closes on Escape and backdrop click unless disabled.
No styles are included. The portal renders a backdrop wrapper with data-modal-backdrop=""; the dialog element is an unstyled child. Style your own content normally:
[data-modal-backdrop] {
position: fixed;
inset: 0;
display: grid;
place-items: center;
background: rgb(0 0 0 / 0.5);
}
.dialog-panel {
max-width: 32rem;
background: white;
padding: 1.5rem;
}npm install
npm run typecheck
npm test
npm run build- No bundled CSS, animation, or visual variants.
- No nested modal stack manager.
- No drawer, sheet, popover, or alert-dialog specialization in v1.