React
Using modals in React
React
Setup
1. Create a Modal Manager
import { createModalManager } from "@jlnstack/modal";
const manager = createModalManager();2. Mount Provider & Outlet
Wrap your app with ModalProvider and place ModalOutlet where you want modals to render:
// app/providers.tsx
"use client";
import { createModalManager } from "@jlnstack/modal";
import { ModalProvider, ModalOutlet } from "@jlnstack/modal/react";
const manager = createModalManager();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ModalProvider manager={manager}>
{children}
<ModalOutlet />
</ModalProvider>
);
}3. Create a Modal
Use the modal builder to define a modal with typed input and output:
// modals/confirm-modal.tsx
import { modal } from "@jlnstack/modal";
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>
));4. Open Modals with useModal
import { useModal } from "@jlnstack/modal/react";
import { confirmModal } from "./modals/confirm-modal";
function DeleteButton() {
const modal = useModal(confirmModal);
const handleDelete = async () => {
const confirmed = await modal.open({
title: "Delete item?",
message: "This action cannot be undone.",
});
if (confirmed) {
deleteItem();
}
};
return <button onClick={handleDelete}>Delete</button>;
}Hooks
useModal
Opens a specific modal and returns a promise that resolves when the modal closes:
import { useModal } from "@jlnstack/modal/react";
function MyComponent() {
const modal = useModal(myModal);
const result = await modal.open({ /* input */ });
// result is typed based on the modal's output type
}useModals
Access all open modals and management actions:
import { useModals } from "@jlnstack/modal/react";
function ModalManager() {
const {
modals, // Array of all open modal instances
count, // Number of open modals
topModal, // The frontmost modal
// Z-index management
bringToFront,
sendToBack,
moveUp,
moveDown,
// Position & size
setPosition,
updatePosition,
setSize,
// Utilities
has,
isOnTop,
close,
closeAll,
} = useModals();
return (
<div>
<p>{count} modals open</p>
<button onClick={closeAll}>Close All</button>
</div>
);
}useModalInstance
Access the current modal instance from within a modal component:
import { useModalInstance } from "@jlnstack/modal/react";
function MyModalContent() {
const {
id, // Modal instance ID
close, // Close this modal
resolve, // Resolve with a value
setPosition, // Set absolute position
updatePosition, // Move by delta
setSize, // Set size
} = useModalInstance();
return (
<div>
<button onClick={() => updatePosition({ x: 10, y: 0 })}>
Move Right
</button>
<button onClick={close}>Close</button>
</div>
);
}useModalManager
Direct access to the modal manager:
import { useModalManager } from "@jlnstack/modal/react";
function MyComponent() {
const manager = useModalManager();
// Full manager API available
manager.open(myModal, { input: "value" });
manager.closeAll();
}Position & Size Management
Each modal instance has a position and size that can be controlled programmatically:
import { useModals } from "@jlnstack/modal/react";
function WindowManager() {
const { modals, setPosition, setSize, bringToFront } = useModals();
return (
<div>
{modals.map((modal) => (
<div
key={modal.id}
style={{
position: "absolute",
left: modal.position.x,
top: modal.position.y,
width: modal.size.width,
height: modal.size.height,
zIndex: modal.order,
}}
onClick={() => bringToFront(modal.id)}
>
{modal.render()}
</div>
))}
</div>
);
}Position Methods
// Set absolute position
setPosition(modalId, { x: 100, y: 100 });
// Move by delta
updatePosition(modalId, { x: 10, y: 0 }); // Move 10px rightSize Methods
// Set size
setSize(modalId, { width: 600, height: 400 });Z-Index Management
Control modal stacking order:
const { bringToFront, sendToBack, moveUp, moveDown, isOnTop } = useModals();
// Bring modal to front of stack
bringToFront(modalId);
// Send modal to back of stack
sendToBack(modalId);
// Move one level up/down
moveUp(modalId);
moveDown(modalId);
// Check if modal is on top
if (isOnTop(modalId)) {
// This modal is the frontmost
}Modal Registry Pattern
For larger applications, organize modals in a registry:
// modals/index.ts
import { modal } from "@jlnstack/modal";
export const modals = {
confirm: 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>
)),
settings: {
theme: modal
.input<{ currentTheme: string }>()
.create((input) => (
<div className="modal">
<h2>Theme Settings</h2>
<p>Current: {input.currentTheme}</p>
</div>
)),
profile: modal
.input<{ userId: string }>()
.create((input) => (
<div className="modal">
<h2>Profile Settings</h2>
<p>User: {input.userId}</p>
</div>
)),
},
};Access modals with dot notation:
import { useModal } from "@jlnstack/modal/react";
import { modals } from "./modals";
function SettingsButton() {
const themeModal = useModal(modals.settings.theme);
return (
<button onClick={() => themeModal.open({ currentTheme: "dark" })}>
Theme Settings
</button>
);
}Lazy Loading in Registry
Use lazy() to wrap modal imports for code-splitting:
// modals/settings.ts - a normal modal file
import { modal } from "@jlnstack/modal";
export const settingsModal = modal
.input<{ userId: string }>()
.create((input, { close }) => (
<div>
<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 with the registry
confirm: modal.input<{ message: string }>().create(/* ... */),
// Lazy - loaded on first open
settings: lazy(() => import("./settings").then(m => m.settingsModal)),
// Lazy with custom fallback
dashboard: lazy(
() => import("./dashboard").then(m => m.dashboardModal),
{ fallback: <div>Loading dashboard...</div> }
),
};Types
import type {
Modal,
ModalInstance,
ModalManager,
Position,
Size,
} from "@jlnstack/modal/react";
// Position type
type Position = { x: number; y: number };
// Size type
type Size = { width: number; height: number };
// Modal instance (runtime)
type ModalInstance = {
id: string;
order: number;
position: Position;
size: Size;
render: () => ReactNode;
resolve: (value: unknown) => void;
close: () => void;
};