react-modal-e2z
v3.0.0
Published
A lightweight, headless, fully controlled React modal built on portals. Hook-first, composable, SSR-safe and React 18 ready.
Maintainers
Readme
🪟 react-modal-e2z
LIVE EXAMPLE
Lightweight, portal-based modal for React (18+).
Fully controlled. Zero hidden state. Deterministic close reasons.
Why react-modal-e2z?
- 🧩 Fully controlled via
isOpen - 🚪 Single
onClose(reason)exit point - 🧠 Clear separation: Layout / Header / Footer / Behavior
- 🎯 No hidden auto-close logic
- 📦 Lightweight & dependency-free
- 📱 Mobile-friendly (fullscreen, full-width buttons)
Designed for explicit UI flows and intent-driven architectures.
Installation
npm install react-modal-e2zImport styles once:
import "react-modal-e2z/build/styles.css";Basic Usage
import React from "react";
import Modal, { IFModalCloseReason } from "react-modal-e2z";
export function Example() {
const [open, setOpen] = React.useState(false);
const handleClose = (reason: IFModalCloseReason) => {
console.log("Closed by:", reason);
setOpen(false);
};
return (
<>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Modal
isOpen={open}
onClose={handleClose}
modalTitle="Simple Modal"
center
>
This is modal content.
</Modal>
</>
);
}Close Reasons
type IFModalCloseReason =
| "escape"
| "backdrop"
| "close-button"
| "cancel"
| "programmatic";Examples
Confirm Dialog
<Modal
isOpen={open}
onClose={(reason) => {
console.log("Close reason:", reason);
setOpen(false);
}}
modalTitle="Confirm Action"
showFooter
alignFooter="right"
showCancelButton
showAcceptButton
labelCancel="Cancel"
labelAccept="Confirm"
onCancel={() => console.log("Cancelled")}
onAccept={() => console.log("Accepted")}
>
Are you sure you want to continue?
</Modal>Custom Footer
<Modal
isOpen={open}
onClose={() => setOpen(false)}
showFooter
modalFooter={
<div style={{ display: "flex", justifyContent: "space-between" }}>
<button onClick={() => setOpen(false)}>Custom Close</button>
<button>Another Action</button>
</div>
}
>
Custom footer content
</Modal>🧠 useModalController
Lightweight stack-based modal controller.
Supports:
- Multiple stacked modals
- Close only top modal
- Replace current modal
- Close all modals
Usage
import Modal, { useModalController } from "react-modal-e2z";
function Example() {
const modal = useModalController();
return (
<>
<button onClick={() => modal.open("A")}>
Open Modal A
</button>
<Modal
isOpen={modal.isOpen("A")}
onClose={modal.close}
>
<h2>Modal A</h2>
<button onClick={() => modal.open("B")}>
Open Modal B
</button>
</Modal>
<Modal
isOpen={modal.isOpen("B")}
onClose={modal.close}
>
<h2>Modal B</h2>
</Modal>
</>
);
}Behavior Model
| Action | Stack |
|--------|-------|
| open("A") | [A] |
| open("B") | [A, B] |
| close() | [A] |
| closeAll() | [] |
Only the top modal responds to:
- Escape
- Backdrop click
- Scroll lock
- Focus trap
Props
Core
| Prop | Type | Description |
|------|------|------------|
| isOpen | boolean | Controls visibility |
| onClose | (reason: IFModalCloseReason) => void | Close callback |
| children | ReactNode | Body content |
Identity
| Prop | Type |
|------|------|
| id? | string |
| instanceId? | string |
Layout
| Prop | Type | Description |
|------|------|------------|
| center? | boolean | Center modal |
| fullScreen? | boolean | Fullscreen mode |
| className? | string | Custom class |
| tabIndex? | number | Root tabIndex |
| height? | number \| string | Fixed height |
| maxHeight? | number \| string | Max height |
| width? | number \| string | Fixed width |
| maxWidth? | number \| string | Max width |
| backdropDark? | boolean | Darker backdrop |
Header
| Prop | Type | Description |
|------|------|------------|
| showHeader? | boolean | Toggle header |
| modalTitle? | string | Title text |
| modalHeader? | ReactNode | Custom header |
| showSplitBorder? | boolean | Split border |
| expandableTitle? | boolean | Double-click title expand |
| onToggle? | (expanded: boolean) => void | Expand callback |
Footer
| Prop | Type |
|------|------|
| showFooter? | boolean |
| modalFooter? | ReactNode |
| alignFooter? | "left" \| "right" \| "center" \| string |
| fullButtonMobile? | boolean |
Buttons
| Prop | Type |
|------|------|
| showExtraButton? | boolean |
| showAcceptButton? | boolean |
| showCancelButton? | boolean |
| showCloseButton? | boolean |
| classNameAccept? | string |
| classNameCancel? | string |
| classNameExtra? | string |
| labelAccept? | string |
| labelCancel? | string |
| labelExtra? | string |
| onAccept? | () => void |
| onCancel? | () => void |
| onExtra? | () => void |
Behavior
| Prop | Type | Description |
|------|------|------------|
| lockBodyScroll? | boolean | Prevent body scroll |
| closeOnEscape? | boolean | Close on ESC |
| closeOnBackdrop? | boolean | Close on outside click |
Button CSS Classes
modal2-primary-buttonmodal2-light-buttonmodal2-danger-buttonmodal2-secondary-button
When to Use
- You want a predictable, controlled modal
- You manage state externally
- You prefer explicit close reasons
License
MIT
