@patch-kit/modal
v1.0.0
Published
Modal Engine for React
Readme
@patch-kit/modal
A stack-based modal engine built with Zustand and React Portals.
The engine owns: stacking, z-index, focus trapping, body scroll lock, escape key, and outside-click dismissal. The consumer owns: all visual styling — overlay color, dialog shape, typography, animations.
Folder contents
useModal.tsx— Zustand store,useModalhook, and shared modal types.ModalItem.tsx— Single modal instance renderer (overlay + dialog).ModalRenderer.tsx— Root renderer that portals the modal stack.index.ts— Public exports.
Exports
useModal— Hook that exposesshowModal,closeModal,closeAllModals.ModalRenderer— Component that renders all active modals todocument.body.ModalItem— Low-level modal renderer used byModalRenderer.
How it works
- Modals are stored in a stack (
ModalInstance[]) in Zustand. showModalpushes a new modal, or replaces an existing one by id.closeModal()closes the top modal;closeModal(id)closes a specific one.ModalRenderermaps the stack toModalItemand portals it outside the React tree.- Each modal gets its own overlay and dialog layer with incremental z-index.
Usage
// 1) Render the engine once, near the app root
<ModalRenderer />
// 2) Open/close modals from anywhere
const { showModal, closeModal } = useModal();
showModal(
<YourStyledModal onClose={() => closeModal()} />,
{
closeOnOutsideClick: true, // default: true
id: "optional-custom-id", // default: auto-generated UUID
ariaLabel: "account settings",
ariaDescribedBy: "settings-desc",
}
);The content you pass to showModal is rendered inside the engine's dialog wrapper.
Your component is responsible for all visual styling — size, color, padding, borders, etc.
Options
showModal(content, options) accepts:
id: string — If provided and already open, the modal is replaced.closeOnOutsideClick: boolean — Closes on overlay click and Escape. Default:true.ariaLabel: string — Applied to the dialog for screen readers.ariaDescribedBy: string — ID of a descriptive element inside the modal content.onClose: function — Callback fired when the modal begins closing.
DOM structure
The engine renders three unstyled elements you can target in CSS:
[data-id="<uuid>"] fixed inset-0, centered — the positioning anchor
[modal-backdrop] absolute inset-0 — receives outside-click; style for the backdrop
(dialog wrapper) relative, z-index above backdrop — your content renders hereExample consumer CSS:
[modal-backdrop] {
background: rgba(0, 0, 0, 0.5);
}Accessibility
- Dialog uses
role="dialog"andaria-modal="true". ariaLabelandariaDescribedByoptions are forwarded to the dialog element.- Focus is trapped within the top modal while it is open.
- The top modal is auto-focused when it becomes active.
Behavior details
- Stacking — Overlays use z-index
1100 + index * 2; dialogs use+ 1. - Escape key — Closes the top modal if
closeOnOutsideClickistrue. - Outside click — Clicking the backdrop closes when
closeOnOutsideClickistrue. - Scroll lock — Body scroll is disabled while any modal is open; scrollbar width is compensated via
padding-rightto prevent layout shift. - SSR safety — Rendering is skipped until the component is mounted on the client.
Notes
ModalItemis exported for advanced use cases; the normal path isuseModal+ModalRenderer.- If you pass
ariaDescribedBy, ensure the referenced element ID exists inside the modal content.
