@rokku-x/react-hook-modal
v0.10.1
Published
A lightweight, powerful, and flexible React modal hook library that supports stacking and multi-window modals, with multiple rendering modes, dynamic and static modal support.
Downloads
351
Maintainers
Readme
react-hook-modal
A lightweight, powerful, and flexible React modal hook library that supports stacking and multi-window modals, with multiple rendering modes, dynamic and static modal support, and zero non-core dependencies.
Installation
npm install @rokku-x/react-hook-modal
# or
bun add @rokku-x/react-hook-modal
# or
yarn add @rokku-x/react-hook-modal
# or
pnpm add @rokku-x/react-hook-modalFeatures
- 🎯 Multiple Render Modes -
STACKED,CURRENT_ONLY,CURRENT_HIDDEN_STACK - 🧱 Stacking & Multi-Window - Open multiple modals at once and control focus/order
- ⚡ Static and Dynamic Modals - Choose the best approach for your use case
- 🪝 React Hooks API - Easy-to-use hook-based interface
- 📦 TypeScript Support - Full type safety out of the box
- 🎨 Customizable Styling - Extensive styling props for complete control
- 🔹 Versatile Overlays - Use it to build dialogs, alerts, popups, prompts, forms, loading spinners, UI blockers, and more.
- ✨ Auto-injected Styles - CSS automatically injected, no separate import needed
- 🪶 Lightweight - Under 3 kB gzipped, tree-shakeable with modular build
- ♿ Accessibility - Built-in support for scroll prevention and inert attribute
Use Cases
- Confirmation & Alerts — Small confirm/cancel dialogs for destructive actions and important messages.
- Prompts & Forms — Collect user input using forms rendered inside modals (supports dynamic portals or static form content).
- Loading & Activity Spinners — Full-screen or localized spinners during async operations; show one-off instances while loading.
- UI Blockers & Backdrops — Block user interaction and display status or retry actions when needed.
- Popups & Tool Panels — Lightweight contextual overlays or tool panels rendered into a modal window.
- Stacked & Multi-instance Modals — Open multiple stacked modals or use multiple
BaseModalRendererinstances (viarendererId) to scope overlays to different app areas, apply different styles to different renderers.
Quick Start
1. Setup the Base Modal Renderer
First, add the BaseModalRenderer at the root of your application:
import { BaseModalRenderer, RenderMode } from '@rokku-x/react-hook-modal';
function App() {
return (
<>
<YourComponents />
<BaseModalRenderer renderMode={RenderMode.STACKED} />
</>
);
}You can mount multiple BaseModalRenderer instances — each must have a unique id to use an isolated modal store. When using hooks, pass rendererId to target a specific renderer's store. Example:
// Mount two separate renderers (different areas of the app)
<BaseModalRenderer id="sidebar-modals" />
<BaseModalRenderer id="dialog-modals" />
// Target a renderer from a hook
const [showSidebarModal] = useStaticModal({ rendererId: 'sidebar-modals' })
const [renderDialog, openDialog] = useDynamicModal({ rendererId: 'dialog-modals' })2. Use the Modal Hook
import { useStaticModal } from '@rokku-x/react-hook-modal';
function MyComponent() {
const [showModal, closeModal] = useStaticModal();
return (
<button onClick={() => showModal(
<div className="modal-content">
<h2>Hello Modal!</h2>
<button onClick={closeModal}>Close</button>
</div>
)}>
Open Modal
</button>
);
}API Reference
Hooks Summary
| Hook | Args | Returns | Description |
|------|------|---------|-------------|
| useBaseModal | { rendererId?: string } | BaseModal actions & state | Low-level store access for advanced operations. Pass rendererId to target a renderer instance. |
| useStaticModal | { rendererId?: string; element?: AcceptableElement } | [showModal, closeModal, focus, updateModalContent, id] | Static modal hook; open named or one-off instances, default content via element. Use rendererId to target a specific renderer. |
| useDynamicModal | { rendererId?: string } | [renderModalElement, showModal, closeModal, focus, id, isForeground] | Dynamic modal hook (portal-rendered content). showModal returns the modal id string. |
BaseModalRenderer
The main component that renders all modals in your application. Must be mounted at the root level.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| id | string | 'base-modal-wrapper' | Unique identifier for the modal wrapper for creating isolated modal stores instance. |
| renderMode | RenderMode | RenderMode.STACKED | Determines how multiple modals are rendered |
| style | CSSProperties | undefined | Inline styles for the dialog element |
| className | string | undefined | CSS class for the dialog element |
| windowClassName | string | undefined | CSS class applied to each modal window |
| windowStyle | CSSProperties | undefined | Inline styles for each modal window |
| disableBackgroundScroll | boolean | true | Prevents body scroll when modal is open |
Render Modes
| Mode | Behavior |
|------|----------|
| STACKED | All modals are visible and stacked on top of each other |
| CURRENT_ONLY | Only the topmost modal is visible |
| CURRENT_HIDDEN_STACK | All modals are in the DOM but only the topmost is visible (best for preserving state) |
Helper Components: ModalBackdrop & ModalWindow
Two small presentational components are exported for convenience to simplify markup and styling.
ModalBackdrop— A full-screen backdrop used to wrap the background and capture clicks (commonly used to close the modal). Accepts standarddivprops:onClick,className,style, etc.ModalWindow— A styled window container used to render modal content. Accepts standarddivprops and prevents event propagation from clicks inside the window.
Example usage:
import { useStaticModal, ModalBackdrop, ModalWindow, BaseModalRenderer } from '@rokku-x/react-hook-modal';
function BackdropExample() {
const [showModal, closeModal] = useStaticModal();
return (
<button onClick={() => showModal(
<ModalBackdrop onClick={closeModal}>
<ModalWindow>
<h2>Title</h2>
<p>Content inside modal window.</p>
<button onClick={closeModal}>Close</button>
</ModalWindow>
</ModalBackdrop>
)}>
Show Modal with Backdrop
</button>
)
}Using these helper components keeps modal markup consistent across your app and makes it easier to style/backdrop behavior.
useStaticModal
Hook for displaying static modal content (content set when the modal is opened).
Returns
[showModal, closeModal, focus, updateModalContent, id]| Return Value | Type | Description |
|---|---|---|
| showModal(el?: AcceptableElement, id?: string) | (el?: AcceptableElement, id?: string) => string | Show a modal and target/reopen an instance with the provided id. Passing the same id will update/replace that instance. Returns the hook's id. |
| showModal(el?: AcceptableElement, createNew?: boolean) | (el?: AcceptableElement, createNew?: boolean) => string | Show a modal and create a new unique instance when createNew === true. Omit or pass false to use the hook's own id (single-instance default). Returns the hook's id. |
| closeModal | (customId?: string) => boolean | Close the given modal instance (defaults to the hook's id) |
| focus | (customId?: string) => boolean | Bring the given modal to the foreground (defaults to the hook's id) |
| updateModalContent | (newContent: AcceptableElement, customId?: string) => boolean | Update the content of the specified modal instance without closing |
| id | string | Unique identifier assigned to this hook instance |
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| element | AcceptableElement | undefined | Default content to display if showModal is called without arguments |
💡 Tip: You can now open multiple instances from a single
useStaticModalhook by passing a custom id ortruetoshowModal. Passing the same custom id will update/replace that instance instead of creating a new one.
Overloads & instance control
showModal(el?: AcceptableElement, id?: string)— Open or target a named instance. Passing the sameidwill update/replace that instance.showModal(el?: AcceptableElement, createNew?: boolean)— Passtrueto create a new unique instance each call. Omit or passfalseto use the hook's own (single) id.
Examples:
const [showModal, closeModal, focus, updateModalContent, id] = useStaticModal()
// Use hook id (single-instance)
showModal(<div>Default</div>)
// Create a unique one-off instance
const oneOffHookId = showModal(<div>One-off</div>, true)
console.log('one-off created by hook id:', oneOffHookId)
// Open or update a named instance
const myModalHookId = showModal(<div>Named</div>, 'my-modal')
console.log('named instance hook id (always hook id):', myModalHookId)
// Update a named instance's content later
updateModalContent(<div>Updated</div>, 'my-modal')
// Close a named instance
closeModal('my-modal')Note:
showModalreturns the hook'sid(string) — the generated or hook id, not a close function.
Migration (from older versions)
If you used the old return signature or relied on showModal returning a close function, update your code as follows:
- const [show, close, focus] = useStaticModal()
- const closeFn = show(<div>Hi</div>)
+ const [show, close, focus, updateModalContent, id] = useStaticModal()
+ const hookId = show(<div>Hi</div>) // returns hook id string
+ // for one-off instances use: show(<div>One-off</div>, true)
+ // for named instances use: show(<div>Named</div>, 'custom-id')The new overloads also allow opening multiple live static instances via custom ids or true for one-off instances.
useDynamicModal
Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| rendererId | string | 'default' | ID of the BaseModalRenderer to target. Use to scope dynamic modals to a specific renderer instance. |
Returns
| Return | Type | Description |
|--------|------|-------------|
| renderModalElement | (el: ReactNode) => ReactNode | Renders content into the modal window via a portal (returns a React portal when renderer is mounted). |
| showModal | () => string | Opens the modal and returns the modal id string. |
| closeModal | () => boolean | Closes the modal instance. |
| focus | () => boolean | Brings this modal to the foreground. |
| id | string | Hook instance id. |
| isForeground | boolean | Whether this modal is currently focused. |
Hook for rendering dynamic modal content (content rendered from within the modal). Use the rendererId option to scope dynamic modals to a specific renderer instance.
useBaseModal
Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| rendererId | string | 'default' | The rendererId identifies which BaseModalRenderer store to target. |
Returns
| Action/Property | Type | Description |
|-----------------|------|-------------|
| pushModal | (modalId: string, el: AcceptableElement, isDynamic?: boolean) => string | Pushes a modal into the store and returns the modal id. |
| popModal | (modalId: string) => boolean | Removes a modal by id. |
| updateModal | (modalId: string, newContent: AcceptableElement) => boolean | Updates content for static modals. |
| focusModal | (modalId: string) => boolean | Moves a modal to the top of the stack. |
| getModal | (modalId: string) => [AcceptableElement | null, boolean] | undefined | Retrieves a modal entry. |
| getModalWindowRef | (modalId: string) => HTMLDivElement | undefined | Returns the modal window DOM node for a given id. |
| getModalOrderIndex | (modalId: string) => number | Returns the modal order index (zero-based). |
| currentModalId | string? | Id of the modal currently in focus. |
| modalStackMap | Map<string, [AcceptableElement | null, boolean]> | Internal stack map of modal entries. |
Low-level hook for direct modal store access for creating advanced custom modals. Used internally by other hooks.
Examples
Quick Examples
Confirm / Alert
function ConfirmExample() {
const [showModal, closeModal] = useStaticModal()
const openConfirm = () => showModal(
<div>
<h2>Confirm Delete</h2>
<p>Are you sure you want to delete this?</p>
<button onClick={() => { console.log('deleted'); closeModal(); }}>Delete</button>
<button onClick={closeModal}>Cancel</button>
</div>
)
return <button onClick={openConfirm}>Delete</button>
}Loading Spinner
function LoadingExample() {
const [showModal, closeModal] = useStaticModal()
const runTask = async () => {
const spinnerId = showModal(<div className="spinner">Loading…</div>, true)
await someAsyncWork()
closeModal(spinnerId)
}
return <button onClick={runTask}>Run Task</button>
}Prompt / Form (dynamic)
function PromptExample() {
const [renderModalElement, showModal, closeModal] = useDynamicModal()
const onOpen = () => showModal()
return (
<>
<button onClick={onOpen}>Open Prompt</button>
{renderModalElement(
<form onSubmit={(e) => { e.preventDefault(); /* read value */ closeModal() }}>
<input name="value" placeholder="Enter value" />
<button type="submit">OK</button>
<button type="button" onClick={closeModal}>Cancel</button>
</form>
)}
</>
)
}Example 1: Basic Static Modal
import { useStaticModal, BaseModalRenderer, ModalBackdrop, ModalWindow } from '@rokku-x/react-hook-modal';
function App() {
return (
<>
<BasicModalExample />
<BaseModalRenderer />
</>
);
}
function BasicModalExample() {
const [showModal, closeModal] = useStaticModal();
return (
<button onClick={() => showModal(
<ModalBackdrop onClick={closeModal}>
<ModalWindow style={{ padding: '20px', background: 'white', borderRadius: '8px' }}>
<h2>Welcome</h2>
<p>This is a basic modal example.</p>
<button onClick={closeModal}>Close</button>
</ModalWindow>
</ModalBackdrop>
)}>
Show Modal
</button>
);
}Example 1b: useStaticModal - Content in Hook Parameter
Pass the modal content directly to the useStaticModal hook. Call showModal() without arguments to display it.
import { useStaticModal, BaseModalRenderer, ModalBackdrop, ModalWindow } from '@rokku-x/react-hook-modal';
function WelcomeModal() {
const [showModal, closeModal] = useStaticModal({
element: (
<ModalBackdrop>
<ModalWindow style={{ padding: '20px', background: 'white', borderRadius: '8px' }}>
<h2>Welcome</h2>
<p>This modal content is defined in the hook parameter.</p>
<button onClick={closeModal}>Close</button>
</ModalWindow>
</ModalBackdrop>
)
});
return <button onClick={showModal}>Show Welcome Modal</button>;
}
function App() {
return (
<>
<WelcomeModal />
<BaseModalRenderer />
</>
);
}Example 1c: useStaticModal - Content in showModal Call
Pass the modal content when calling showModal(). This approach is more flexible for dynamic content.
import { useStaticModal, BaseModalRenderer, ModalBackdrop, ModalWindow } from '@rokku-x/react-hook-modal';
function DynamicContentModal() {
const [showModal, closeModal] = useStaticModal();
const showWelcome = () => {
showModal(
<ModalBackdrop onClick={closeModal}>
<ModalWindow style={{ padding: '20px', background: 'white', borderRadius: '8px' }}>
<h2>Welcome</h2>
<p>This modal content is provided when calling showModal().</p>
<button onClick={closeModal}>Close</button>
</ModalWindow>
</ModalBackdrop>
);
};
const showGoodbye = () => {
showModal(
<ModalBackdrop onClick={closeModal}>
<ModalWindow style={{ padding: '20px', background: 'white', borderRadius: '8px' }}>
<h2>Goodbye</h2>
<p>Different content can be shown by calling showModal with different content.</p>
<button onClick={closeModal}>Close</button>
</ModalWindow>
</ModalBackdrop>
);
};
return (
<>
<button onClick={showWelcome}>Show Welcome</button>
<button onClick={showGoodbye}>Show Goodbye</button>
</>
);
}
function App() {
return (
<>
<DynamicContentModal />
<BaseModalRenderer />
</>
);
}Example 2: Confirmation Dialog
import { useStaticModal, BaseModalRenderer, ModalBackdrop, ModalWindow } from '@rokku-x/react-hook-modal';
function ConfirmationExample() {
const [showModal, closeModal] = useStaticModal();
const handleDelete = () => {
showModal(
<ModalBackdrop onClick={closeModal}>
<ModalWindow style={{ padding: '20px', background: 'white', borderRadius: '8px', maxWidth: '400px' }}>
<h2>Confirm Delete</h2>
<p>Are you sure you want to delete this item?</p>
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
<button onClick={closeModal}>Cancel</button>
<button
onClick={() => {
console.log('Deleted!');
closeModal();
}}
style={{ background: '#dc3545', color: 'white' }}
>
Delete
</button>
</div>
</ModalWindow>
</ModalBackdrop>
);
};
return <button onClick={handleDelete}>Delete Item</button>;
}
function App() {
return (
<>
<ConfirmationExample />
<BaseModalRenderer />
</>
);
}Example 3: Update Modal Content
import { useStaticModal, BaseModalRenderer, ModalBackdrop, ModalWindow } from '@rokku-x/react-hook-modal';
import { useState } from 'react';
function UpdateableModalExample() {
const [showModal, closeModal, focus, updateModalContent, modalId] = useStaticModal();
const [step, setStep] = useState(0);
const openWizard = () => {
setStep(0);
showModal(
<ModalBackdrop>
<ModalWindow style={{ padding: '20px', background: 'white', borderRadius: '8px', maxWidth: '500px' }}>
{renderStep(0)}
</ModalWindow>
</ModalBackdrop>
);
};
const renderStep = (stepNum: number) => (
<div>
<h2>Step {stepNum + 1} of 3</h2>
<p>This is step {stepNum + 1} content.</p>
<div style={{ display: 'flex', gap: '10px', marginTop: '20px' }}>
<button
onClick={() => {
const newStep = stepNum - 1;
setStep(newStep);
updateModalContent(renderStep(newStep));
}}
disabled={stepNum === 0}
>
Previous
</button>
<button
onClick={() => {
if (stepNum === 2) {
console.log('Wizard completed!');
closeModal();
} else {
const newStep = stepNum + 1;
setStep(newStep);
updateModalContent(renderStep(newStep));
}
}}
>
{stepNum === 2 ? 'Complete' : 'Next'}
</button>
</div>
</div>
);
return <button onClick={openWizard}>Open Wizard</button>;
}
function App() {
return (
<>
<UpdateableModalExample />
<BaseModalRenderer />
</>
);
}Example 4: Dynamic Modal with Form
import { useDynamicModal, BaseModalRenderer, ModalBackdrop, ModalWindow } from '@rokku-x/react-hook-modal';
import { useState } from 'react';
function FormModal() {
const [renderModalElement, showModal, closeModal, focus, id, isForeground] = useDynamicModal();
const [formData, setFormData] = useState({ name: '', email: '' });
const openModal = () => {
showModal();
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Form submitted:', formData);
closeModal();
};
const modalContent = (
<ModalBackdrop onClick={closeModal}>
<ModalWindow style={{ padding: '20px', background: 'white', borderRadius: '8px', maxWidth: '400px' }}>
<form onSubmit={handleSubmit}>
<h2>Contact Form</h2>
<input
type="text"
placeholder="Name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
style={{ width: '100%', marginBottom: '10px', padding: '8px' }}
/>
<input
type="email"
placeholder="Email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
style={{ width: '100%', marginBottom: '10px', padding: '8px' }}
/>
<div style={{ display: 'flex', gap: '10px' }}>
<button type="submit">Submit</button>
<button type="button" onClick={closeModal}>Cancel</button>
</div>
</form>
</ModalWindow>
</ModalBackdrop>
);
return (
<div>
<button onClick={openModal}>Open Form</button>
{renderModalElement(modalContent)}
</div>
);
}
function App() {
return (
<>
<FormModal />
<BaseModalRenderer />
</>
);
}Example 5: Multiple Stacked Modals
import { useStaticModal, BaseModalRenderer, RenderMode, ModalBackdrop, ModalWindow } from '@rokku-x/react-hook-modal';
function StackedModalsExample() {
const [showModal1, closeModal1] = useStaticModal();
const [showModal2, closeModal2] = useStaticModal();
const openFirstModal = () => {
showModal1(
<ModalBackdrop onClick={closeModal1}>
<ModalWindow style={{ padding: '20px', background: 'white', borderRadius: '8px', maxWidth: '400px' }}>
<h2>First Modal</h2>
<p>This is the first modal.</p>
<button onClick={() => openSecondModal()}>Open Another</button>
<button onClick={closeModal1}>Close</button>
</ModalWindow>
</ModalBackdrop>
);
};
const openSecondModal = () => {
showModal2(
<ModalBackdrop onClick={closeModal2}>
<ModalWindow style={{ padding: '20px', background: '#f0f0f0', borderRadius: '8px', maxWidth: '400px' }}>
<h2>Second Modal</h2>
<p>This modal is stacked on top of the first one!</p>
<button onClick={closeModal2}>Close This Modal</button>
</ModalWindow>
</ModalBackdrop>
);
};
return <button onClick={openFirstModal}>Open Stacked Modals</button>;
}
function App() {
return (
<>
<StackedModalsExample />
<BaseModalRenderer renderMode={RenderMode.STACKED} />
</>
);
}Example 5b: Multiple Named & One-off Instances (useStaticModal)
import { useStaticModal, BaseModalRenderer, RenderMode, ModalBackdrop, ModalWindow } from '@rokku-x/react-hook-modal';
function MultipleInstancesExample() {
const [showModal, closeModal] = useStaticModal();
const openTwoDifferent = () => {
// Use custom ids to open/target distinct modal instances
showModal(
<ModalBackdrop onClick={() => closeModal('first-modal')}>
<ModalWindow style={{ padding: '20px', background: 'white', borderRadius: '8px', maxWidth: '400px' }}>
<h2>First modal</h2>
<p>A modal opened using a custom id.</p>
<button onClick={() => closeModal('first-modal')}>Close</button>
</ModalWindow>
</ModalBackdrop>,
'first-modal'
);
showModal(
<ModalBackdrop onClick={() => closeModal('second-modal')}>
<ModalWindow style={{ padding: '20px', background: '#f6f6f6', borderRadius: '8px', maxWidth: '400px' }}>
<h2>Second modal</h2>
<p>Another modal opened with a different id.</p>
<button onClick={() => closeModal('second-modal')}>Close</button>
</ModalWindow>
</ModalBackdrop>,
'second-modal'
);
};
const openOneOff = () => {
// Pass `true` to create a new unique instance each time
showModal(
<ModalBackdrop>
<ModalWindow style={{ padding: '20px', background: 'white', borderRadius: '8px', maxWidth: '400px' }}>
<h2>One-off</h2>
<p>This creates a unique instance every time (pass `true`).</p>
<button onClick={closeModal}>Close</button>
</ModalWindow>
</ModalBackdrop>,
true
);
};
return (
<>
<button onClick={openTwoDifferent}>Open Two Different Modals</button>
<button onClick={openOneOff}>Open One-off Modal</button>
</>
);
}
function App() {
return (
<>
<MultipleInstancesExample />
<BaseModalRenderer renderMode={RenderMode.STACKED} />
</>
);
}Example 5c: Named & One-off Instances (useStaticModal)
import { useStaticModal, BaseModalRenderer, ModalBackdrop, ModalWindow } from '@rokku-x/react-hook-modal';
function MultipleInstancesAdvanced() {
const [showModal, closeModal, focus, updateModalContent, id] = useStaticModal();
const openNamed = () => {
// Open/replace a named instance called 'named-a'
showModal(
<ModalBackdrop onClick={() => closeModal('named-a')}>
<ModalWindow style={{ padding: '20px', background: 'white' }}>
<h2>Named A</h2>
<p>This modal is opened with a custom id ('named-a').</p>
<button onClick={() => closeModal('named-a')}>Close Named A</button>
</ModalWindow>
</ModalBackdrop>,
'named-a'
);
};
const openOneOff = () => {
// Create a one-off unique instance each time by passing `true`
const oneOffHookId = showModal(
<ModalBackdrop>
<ModalWindow style={{ padding: '20px', background: '#f7f7f7' }}>
<h2>One-off</h2>
<p>This instance is unique every call (created by passing <code>true</code>).</p>
</ModalWindow>
</ModalBackdrop>,
true
);
console.log('one-off created by hook id:', oneOffHookId);
};
const updateNamed = () => {
updateModalContent(
<ModalBackdrop>
<ModalWindow style={{ padding: '20px', background: 'white' }}>
<h2>Named A (Updated)</h2>
<p>Content was updated programmatically via <code>updateModalContent</code>.</p>
</ModalWindow>
</ModalBackdrop>,
'named-a'
);
};
return (
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={openNamed}>Open/Update Named A</button>
<button onClick={openOneOff}>Open One-off</button>
<button onClick={updateNamed}>Update Named A</button>
<div style={{ alignSelf: 'center', marginLeft: 12, color: '#666' }}>hook id: <code>{id}</code></div>
</div>
);
}
function App() {
return (
<>
<MultipleInstancesAdvanced />
<BaseModalRenderer />
</>
);
}This example shows how to open named instances (by passing a string id) and how to create one-off unique instances (by passing
true). It also demonstrates using the returnedidfor reference in your UI.
Example 6: Custom Styling
import { useStaticModal, BaseModalRenderer, ModalBackdrop, ModalWindow } from '@rokku-x/react-hook-modal';
function StyledModalExample() {
const [showModal, closeModal] = useStaticModal();
return (
<>
<button onClick={() => showModal(
<ModalBackdrop onClick={closeModal}>
<ModalWindow style={{
padding: '30px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: '12px',
color: 'white',
textAlign: 'center',
maxWidth: '500px',
boxShadow: '0 10px 40px rgba(0,0,0,0.3)'
}}>
<h2>Styled Modal</h2>
<p>This modal has custom styling!</p>
<button
onClick={closeModal}
style={{
background: 'white',
color: '#667eea',
border: 'none',
padding: '10px 20px',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
Close
</button>
</ModalWindow>
</ModalBackdrop>
)}>
Show Styled Modal
</button>
<BaseModalRenderer
windowClassName="custom-modal-window"
windowStyle={{ backdropFilter: 'blur(5px)' }}
className="custom-modal-wrapper"
/>
</>
);
}Types
RenderMode
enum RenderMode {
STACKED = 0, // All modals visible and stacked
CURRENT_ONLY = 1, // Only topmost modal visible
CURRENT_HIDDEN_STACK = 2 // All in DOM but only topmost visible
}AcceptableElement
type AcceptableElement = React.ReactNode | (() => React.ReactNode);Best Practices
- Always mount
BaseModalRendererat the root - Place it at the top level of your application - Choose the right render mode - Use
STACKEDfor independent modals,CURRENT_HIDDEN_STACKto preserve state - Use static modals for simple content - Faster and simpler than dynamic modals
- Use dynamic modals for complex interactions - Better for forms and interactive content
- Keep modals accessible - Provide keyboard navigation and ARIA attributes
- Customize styling carefully - Use
windowClassNameandwindowStylefor consistent styling
Bundle Size
- ESM: ~2.83 kB gzipped (7.71 kB raw)
- CJS: ~2.78 kB gzipped (8.56 kB raw)
Measured with Vite build for v0.9.2 (includes auto-injected CSS, total size of all modules).
Runtime Performance
Measured locally (jsdom, React 18, macOS) using a microbenchmark:
- Push/Pop: ~7.3 µs per op
- Update Content: ~2.4 µs per op
- Focus Modal: ~1.1 µs per op
- Dynamic Portal Render: ~0.5 µs per call
Notes:
- Numbers are approximate and depend on device/load.
- Portal render figures reflect the cost of creating the portal when the modal window ref exists.
Library Load Time
Measured in Node (dynamic import/require):
- CJS require: ~20.5 ms
- ESM import: ~38.2 ms
See perf harnesses under src/__tests__/perf.test.ts and src/__tests__/perf-load.test.ts.
License
MIT
