Skip to main content

Examples

Practical usage patterns for common modal scenarios.


Basic alert / confirm / prompt

import { alert, confirm, prompt } from '@lerx/promise-modal';

// Alert — resolves void on close
await alert({ title: 'Done', content: 'Your changes were saved.' });

// Confirm — resolves boolean
const ok = await confirm({
title: 'Delete item',
content: 'This cannot be undone.',
footer: { confirm: 'Delete', cancel: 'Cancel' },
});

// Prompt — resolves typed value
const name = await prompt<string>({
title: 'Enter name',
defaultValue: '',
Input: ({ value, onChange }) => (
<input value={value ?? ''} onChange={e => onChange(e.target.value)} />
),
disabled: v => !v || v.trim().length < 2,
});

Subtypes

The subtype prop is forwarded to component slots so you can apply different styles per semantic type.

await alert({ title: 'Info',    content: '...', subtype: 'info' });
await alert({ title: 'Success', content: '...', subtype: 'success' });
await alert({ title: 'Warning', content: '...', subtype: 'warning' });
await alert({ title: 'Error', content: '...', subtype: 'error' });

await alert({
title: 'Notice',
content: 'Please acknowledge.',
footer: { confirm: 'I understand' },
});

const result = await confirm({
title: 'Publish?',
content: 'This will go live immediately.',
footer: { confirm: 'Publish now', cancel: 'Not yet' },
});

Prompt with complex value

type UserInfo = { name: string; age: number };

const user = await prompt<UserInfo>({
title: 'User info',
defaultValue: { name: '', age: 0 },
Input: ({ value, onChange }) => (
<div>
<input
value={value?.name ?? ''}
onChange={e => onChange({ ...value!, name: e.target.value })}
placeholder="Name"
/>
<input
type="number"
value={value?.age ?? 0}
onChange={e => onChange({ ...value!, age: Number(e.target.value) })}
placeholder="Age"
/>
</div>
),
disabled: v => !v?.name,
});

Custom modal components

Override component slots globally on ModalProvider or per-call on individual modal options.

import { ModalProvider, alert, type ModalFrameProps, type WrapperComponentProps, type FooterComponentProps } from '@lerx/promise-modal';

// Foreground — receives ModalFrameProps + children
const MyForeground = ({ children, type }: React.PropsWithChildren<ModalFrameProps>) => (
<div style={{ background: '#fff', borderRadius: 12, padding: 24, maxWidth: 480 }}>
{children}
</div>
);

// Title wrapper
const MyTitle = ({ children }: WrapperComponentProps) => (
<h2 style={{ margin: '0 0 8px' }}>{children}</h2>
);

// Footer — receives onConfirm, onCancel, disabled, labels
const MyFooter = ({ onConfirm, onCancel, confirmLabel, cancelLabel, disabled }: FooterComponentProps) => (
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 24 }}>
{onCancel && <button onClick={onCancel}>{cancelLabel ?? 'Cancel'}</button>}
<button onClick={onConfirm} disabled={disabled}>{confirmLabel ?? 'OK'}</button>
</div>
);

function App() {
return (
<ModalProvider
ForegroundComponent={MyForeground}
TitleComponent={MyTitle}
FooterComponent={MyFooter}
>
<YourApp />
</ModalProvider>
);
}

Per-call component override

await alert({
title: 'Special modal',
content: 'Uses a different foreground just for this call.',
ForegroundComponent: ({ children }) => (
<div style={{ background: '#f0f8ff', padding: 32, borderRadius: 16 }}>
{children}
</div>
),
});

Background data

Pass typed data to BackgroundComponent via background.data.

type BgData = 'danger' | 'normal';

const MyBackground = ({ background, children, onClick }: ModalFrameProps<object, BgData>) => {
const color = background?.data === 'danger' ? 'rgba(200,0,0,0.4)' : 'rgba(0,0,0,0.35)';
return (
<div style={{ position: 'fixed', inset: 0, background: color, display: 'flex', alignItems: 'center', justifyContent: 'center' }} onClick={onClick}>
{children}
</div>
);
};

await confirm({
title: 'Delete account',
content: 'This is permanent.',
background: { data: 'danger' },
});

Async operations in modals

async function handleSubmit() {
const confirmed = await confirm({
title: 'Submit order',
content: 'Proceed with checkout?',
});
if (!confirmed) return;

// Show a loading-style alert while work completes
const done = submitOrder();
await alert({
title: 'Processing',
content: 'Please wait...',
manualDestroy: true, // keep alive until we call onDestroy
closeOnBackdropClick: false,
footer: false,
});
await done;
}

Nested / sequential modals

async function multiStep() {
const go = await confirm({
title: 'Start',
content: 'This takes multiple steps.',
footer: { confirm: 'Continue', cancel: 'Cancel' },
});
if (!go) return;

const name = await prompt<string>({
title: 'Your name',
defaultValue: '',
Input: ({ value, onChange }) => (
<input value={value ?? ''} onChange={e => onChange(e.target.value)} />
),
});
if (!name) return;

const final = await confirm({
title: 'Confirm',
content: `Submit as "${name}"?`,
subtype: 'warning',
});

if (final) {
await alert({ title: 'Done', content: `Submitted as ${name}.`, subtype: 'success' });
}
}

Component-lifecycle-scoped modals (useModal)

Modals opened via useModal are automatically closed when the component unmounts. Useful for forms and detail views.

import { useModal } from '@lerx/promise-modal';

function ItemEditor({ itemId }: { itemId: string }) {
const modal = useModal();

const handleDelete = async () => {
const ok = await modal.confirm({
title: 'Delete item',
content: `Delete item ${itemId}?`,
});
if (ok) await deleteItem(itemId);
};

return <button onClick={handleDelete}>Delete</button>;
// If ItemEditor unmounts while the confirm modal is open, it will be closed automatically.
}

Custom anchor (render modals into a specific DOM node)

import { useRef, useEffect } from 'react';
import { ModalProvider, type ModalProviderHandle, alert } from '@lerx/promise-modal';

function CustomAnchorExample() {
const providerRef = useRef<ModalProviderHandle>(null);
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (providerRef.current && containerRef.current) {
providerRef.current.initialize(containerRef.current);
}
}, []);

return (
<ModalProvider ref={providerRef}>
<div ref={containerRef} style={{ position: 'relative', height: 400 }} />
<button onClick={() => alert({ title: 'In container', content: 'Rendered inside the div above.' })}>
Open
</button>
</ModalProvider>
);
}

Toast notifications

Build toast messages using alert with a custom ForegroundComponent and useDestroyAfter.

import { useRef, useEffect } from 'react';
import {
alert,
type ModalFrameProps,
useModalAnimation,
useModalDuration,
useDestroyAfter,
} from '@lerx/promise-modal';

function ToastForeground({ id, visible, children, onClose, hideAfterMs = 3000 }: React.PropsWithChildren<ModalFrameProps & { hideAfterMs?: number }>) {
const ref = useRef<HTMLDivElement>(null);
const { milliseconds } = useModalDuration();

useEffect(() => {
const t = setTimeout(onClose, hideAfterMs);
return () => clearTimeout(t);
}, [onClose, hideAfterMs]);

useModalAnimation(visible, {
onVisible: () => ref.current?.classList.add('toast--visible'),
onHidden: () => ref.current?.classList.remove('toast--visible'),
});

useDestroyAfter(id, milliseconds);

return (
<div ref={ref} className="toast">
{children}
</div>
);
}

let destroyPrevToast: (() => void) | undefined;

export function toast(message: React.ReactNode, duration = 3000) {
destroyPrevToast?.();
return alert({
content: message,
footer: false,
dimmed: false,
closeOnBackdropClick: false,
ForegroundComponent: (props: ModalFrameProps) => {
destroyPrevToast = props.onDestroy;
return <ToastForeground {...props} hideAfterMs={duration} />;
},
});
}

Context: theme / locale in modal components

Pass shared data to all modal component slots via the context prop on ModalProvider.

// Provider setup
<ModalProvider context={{ theme: 'dark', locale: 'ko-KR' }}>
<App />
</ModalProvider>

// In a custom component slot — context is always injected
const MyTitle = ({ children, context }: WrapperComponentProps<{ theme: string }>) => (
<h2 style={{ color: context.theme === 'dark' ? '#fff' : '#000' }}>{children}</h2>
);

// In a prompt Input — context is also available
const myPrompt = await prompt<string>({
title: 'Name',
defaultValue: '',
Input: ({ value, onChange, context }) => (
<input
value={value ?? ''}
onChange={e => onChange(e.target.value)}
placeholder={context.locale === 'ko-KR' ? '이름 입력' : 'Enter name'}
/>
),
});
AI Agent Reference

AI Agent Reference — Examples Summary

Pattern index

GoalKey APIs
Simple notificationawait alert({ title, content })
Yes/no decisionconst ok = await confirm(...)
User text inputconst val = await prompt<T>({ Input, defaultValue })
Typed input validationprompt({ disabled: v => condition })
Custom lookModalProvider slots or per-call ForegroundComponent
Background dataalert({ background: { data: myData } })
Sequential workflowawait confirm(...) then await prompt(...) then await alert(...)
Auto-close on unmountconst modal = useModal(); await modal.confirm(...)
Custom DOM targetModalProviderHandle.initialize(element)
Toastalert({ footer: false, dimmed: false, ForegroundComponent: toastComponent })
Theme/locale in componentsModalProvider context={...} + WrapperComponentProps.context

Important constraints for code generation

  1. ModalProvider must exist in the React tree before any alert/confirm/prompt call renders.
  2. prompt requires the Input prop — it is not optional.
  3. Custom ForegroundComponent must forward children (it wraps title, content, footer slots).
  4. BackgroundComponent does NOT receive children — it is a sibling layer, not a wrapper.
  5. manualDestroy: true requires calling onDestroy() explicitly; otherwise the modal DOM node leaks.
  6. useDestroyAfter and useModalAnimation must be called inside a ForegroundComponent rendered by the modal system (they read modal context internally).
  7. footer: false hides the footer entirely — no confirm/cancel buttons.
  8. disabled in prompt controls the confirm button state; it does not prevent the modal from being closed via backdrop or cancel.