Introduction
Type-safe modal management for React
Modal
A lightweight, type-safe modal management library for React with support for:
- Type-safe modals with input/output generics
- Builder API with fluent
.input(),.output(),.template()methods - Lazy loading with
lazy()for code-splitting modals - Position & size management for draggable/resizable modals
- Z-index management with bringToFront, sendToBack, moveUp, moveDown
Installation
npm install @jlnstack/modalQuick Start
1. Create a Modal
Use the modal builder to define type-safe modals:
import { modal } from "@jlnstack/modal";
// Simple modal with typed input
export const confirmModal = modal
.input<{ title: string; message: string }>()
.output<boolean>()
.create((input, { resolve, close }) => (
<div className="modal">
<h2>{input.title}</h2>
<p>{input.message}</p>
<button onClick={() => resolve(true)}>Confirm</button>
<button onClick={() => close()}>Cancel</button>
</div>
));2. Set up the Provider
import { createModalManager } from "@jlnstack/modal";
import { ModalProvider, ModalOutlet } from "@jlnstack/modal/react";
const manager = createModalManager();
function App() {
return (
<ModalProvider manager={manager}>
<YourApp />
<ModalOutlet />
</ModalProvider>
);
}3. Open Modals
import { useModal } from "@jlnstack/modal/react";
import { confirmModal } from "./modals";
function DeleteButton() {
const modal = useModal(confirmModal);
const handleDelete = async () => {
const confirmed = await modal.open({
title: "Delete Item",
message: "Are you sure you want to delete this item?",
});
if (confirmed) {
// User clicked confirm
deleteItem();
}
};
return <button onClick={handleDelete}>Delete</button>;
}Modal Builder API
The modal builder provides a fluent API for creating type-safe modals:
import { modal } from "@jlnstack/modal";
// Define input and output types
const myModal = modal
.input<{ name: string }>() // Type the input
.output<string>() // Type the resolved value
.create((input, options) => <MyComponent {...input} {...options} />);With Default Values
Provide default values to make input fields optional:
const greetModal = modal
.input<{ name: string; greeting: string }>()
.create(
(input) => <p>{input.greeting}, {input.name}!</p>,
{
defaultValues: {
modal: { greeting: "Hello" } // greeting is now optional
}
}
);
// Can open without greeting
greetModal.open({ name: "World" });With Templates
Wrap modals in reusable templates:
const dialogModal = modal
.output<boolean>()
.template(({ modal, close, resolve }) => (
<div className="dialog-wrapper">
<div className="dialog-content">{modal}</div>
<div className="dialog-footer">
<button onClick={() => resolve(false)}>Cancel</button>
<button onClick={() => resolve(true)}>OK</button>
</div>
</div>
))
.create((input) => <p>{input.message}</p>);Lazy Loading
Use lazy() to wrap modal imports for code-splitting. The modal is only loaded when first opened:
// modals/settings.ts - a normal modal definition
import { modal } from "@jlnstack/modal";
export const settingsModal = modal
.input<{ userId: string }>()
.create((input, { close }) => (
<div className="modal">
<h2>Settings for {input.userId}</h2>
<button onClick={close}>Close</button>
</div>
));// modals/index.ts - wrap with lazy()
import { modal, lazy } from "@jlnstack/modal";
export const modals = {
// Eager - loaded immediately with the registry
confirm: modal.input<{ message: string }>().create(/* ... */),
// Lazy - only loaded when first opened
settings: lazy(() => import("./settings").then(m => m.settingsModal)),
};You can provide a custom fallback while loading:
import { lazy } from "@jlnstack/modal";
const settingsModal = lazy(
() => import("./settings").then(m => m.settingsModal),
{ fallback: <LoadingSpinner /> }
);