Tailwind CSS
@lamstack/react-dialog doesn’t render anything itself, so styling the alert/confirm/
prompt templates with Tailwind utility classes is just… writing a normal React
component with className props.
Live demo
Try all four ways to open a dialog — the log below each result shows exactly what each call resolved with:
alert()— acknowledgement only, resolvesvoidonce dismissed.confirm()— resolvestrue/falsedepending on which button was pressed.prompt()— resolves the typed string, ornullif cancelled.open(custom)— opensRatingDialog, a component with its own payload ({ itemName: string }) and result (number | null) that has nothing to do with the three built-in kinds above — this is the escape hatch for any dialog shape you need.
Preview
alert()
Acknowledgement only — resolves void once dismissed.
confirm()
Resolves true/false depending on which button was pressed.
prompt()
Resolves the typed string, or null if cancelled.
open(custom)
Any component works, not just alert/confirm/prompt — here, a rating.
Chaining dialogs with async/await
Each call blocks until its dialog closes, so a multi-step flow — confirm, then prompt, then alert — reads top-to-bottom. No nested callbacks, no extra state for tracking which dialog is currently open.
Loading state via async onClose()
onClose runs before the promise resolves — the dialog awaits it itself to show "Saving…" with no extra state passed in from here.
alert and prompt templates follow the same shape — see the
API reference for their exact payload types.
A custom (non-template) dialog
The open(custom) button above opens this — a component with its own payload/result
types, passed directly to open() instead of going through templates:
import * as React from 'react';
import { useDialogs } from '@lamstack/react-dialog';
import type { DialogProps } from '@lamstack/react-dialog';
interface ItemPayload {
itemName: string;
}
function RatingDialog({ payload, open, onClose }: DialogProps<ItemPayload, number | null>) {
const [rating, setRating] = React.useState(0);
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
<div className="w-full max-w-sm rounded-lg border bg-white p-5 dark:bg-neutral-900">
<h3 className="font-semibold">Rate "{payload.itemName}"</h3>
<div className="mt-3 flex gap-1">
{[1, 2, 3, 4, 5].map((star) => (
<button key={star} onClick={() => setRating(star)}>
{star <= rating ? '★' : '☆'}
</button>
))}
</div>
<div className="mt-5 flex justify-end gap-2">
<button onClick={() => onClose(null)}>Skip</button>
<button disabled={rating === 0} onClick={() => onClose(rating)}>
Submit
</button>
</div>
</div>
</div>
);
}
function RateButton() {
const { open } = useDialogs();
return (
<button
onClick={async () => {
const rating = await open(RatingDialog, { itemName: 'Acme Widget' });
console.log(rating); // number, or null if skipped
}}
>
Rate this
</button>
);
}Nothing about open() is special-cased for alert/confirm/prompt — those three just
call open(templates.alert, ...)/open(templates.confirm, ...)/open(templates.prompt, ...) internally. See the API reference for the full breakdown.
Dark mode
This docs site (and this demo) toggle a dark class on <html> via the theme switcher in
the navbar. If your app does the same, add this once to your global CSS so Tailwind’s
dark: variant follows that class instead of the OS prefers-color-scheme media query:
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));