Dialog
Modal component for centered overlay content, usable declaratively or imperatively
Dialog
A modal component that renders content in a centered overlay. It serves as the foundational layer for more complex dialog patterns and handles escape key dismissal and backdrop click behavior.
There are two ways to use it:
- Declarative —
<Dialog open onClose={...}>, when the dialog belongs to a component's state. Same shape asDrawer. - Imperative —
Dialog.show(), for one-off dialogs triggered from anywhere. Multiple dialogs stack, and eachshow()returns an id for targeted dismissal.
Dialogs always render above drawers.
Setup
Dialog.show() requires <GeckoUIProvider> to wrap your app. The declarative <Dialog> renders in place and works without it.
import { Dialog } from '@geckoui/geckoui';Declarative Usage
import { Dialog, Button } from '@geckoui/geckoui';
function Example() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>Open Dialog</Button>
<Dialog open={open} onClose={() => setOpen(false)}>
<h2 className="text-xl font-semibold mb-4">Controlled Dialog</h2>
<Button onClick={() => setOpen(false)}>Close</Button>
</Dialog>
</>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Whether the dialog is visible |
onClose | () => void | — | Called when the dialog asks to close (Esc or backdrop click) |
children | ReactNode | — | Dialog content |
className | string | — | CSS class for the dialog container |
dismissOnEscape | boolean | true | Dismiss dialog on Escape key press |
dismissOnOutsideClick | boolean | true | Dismiss dialog on backdrop click |
data-* | string | — | Data attributes for the dialog element |
Imperative Usage
import { Dialog, Button } from '@geckoui/geckoui';
function Example() {
const openDialog = () => {
Dialog.show({
content: ({ dismiss }) => (
<div>
<h2 className="text-xl font-semibold mb-4">Welcome!</h2>
<p className="text-gray-600 mb-6">
This is a simple dialog with custom content.
</p>
<Button onClick={dismiss}>Close</Button>
</div>
)
});
};
return <Button onClick={openDialog}>Open Dialog</Button>;
}API
Dialog.show(options)
Opens a dialog and returns an id string you can pass to dismiss(id).
| Option | Type | Default | Description |
|---|---|---|---|
content | ReactNode | FC<{ dismiss: () => void }> | — | Content to display in the dialog |
className | string | — | CSS class for the dialog container |
dismissOnEscape | boolean | true | Dismiss dialog on Escape key press |
dismissOnOutsideClick | boolean | true | Dismiss dialog on backdrop click |
data-* | string | — | Data attributes for the dialog element |
Returns: string — the overlay id.
Dialog.dismiss(id?)
Closes a dialog. Pass the id returned by show() to close a specific dialog, or call with no argument to close the topmost dialog. It never closes a drawer — use Drawer.dismiss() for those.
const id = Dialog.show({ content: () => <p>Hello</p> });
// Close this specific dialog:
Dialog.dismiss(id);
// Close whatever dialog is on top:
Dialog.dismiss();Note: clicks inside the dialog never dismiss it. That includes content rendered in a portal, such as a Select or Menu popup — only a click on the backdrop itself closes the dialog.
Examples
Stacking Dialogs
Dialog.show({
content: ({ dismiss }) => (
<div>
<p>First dialog</p>
<Button onClick={() =>
Dialog.show({ content: () => <p>Second dialog (stacked on top)</p> })
}>
Open another
</Button>
<Button onClick={dismiss}>Close this</Button>
</div>
)
});Context Consumption
Overlay content can read any React context provided above GeckoUIProvider:
// Inside a component where AuthContext is available:
Dialog.show({
content: () => {
const { user } = useContext(AuthContext);
return <p>Logged in as {user.name}</p>;
}
});Image Lightbox
const openImageViewer = () => {
Dialog.show({
content: ({ dismiss }) => (
<div className="relative">
<img src="/large-image.jpg" alt="Preview" className="max-w-4xl rounded-lg" />
<button onClick={dismiss} className="absolute top-4 right-4 size-8 flex items-center justify-center bg-black/50 text-white rounded-full">
✕
</button>
</div>
),
className: "bg-transparent shadow-none",
dismissOnEscape: true,
dismissOnOutsideClick: true
});
};Form Modal
import { Input, Label } from '@geckoui/geckoui';
const showUserForm = () => {
Dialog.show({
content: ({ dismiss }) => (
<div>
<h2 className="text-xl font-semibold mb-4">Edit Profile</h2>
<div className="space-y-4">
<div className="space-y-1">
<Label htmlFor="name">Name</Label>
<Input id="name" type="text" defaultValue="John Doe" />
</div>
<div className="flex gap-2 justify-end">
<Button variant="ghost" onClick={dismiss}>Cancel</Button>
<Button onClick={dismiss}>Save Changes</Button>
</div>
</div>
</div>
),
className: "max-w-2xl",
dismissOnEscape: false,
dismissOnOutsideClick: false
});
};Non-Dismissible Dialog
Dialog.show({
content: ({ dismiss }) => (
<div>
<h2 className="text-xl font-semibold mb-4">Important Notice</h2>
<p className="text-gray-600 mb-6">This dialog can only be closed by clicking the button.</p>
<Button onClick={dismiss}>I Understand</Button>
</div>
),
dismissOnEscape: false,
dismissOnOutsideClick: false
});Loading State
Dialog.show({
content: () => (
<div className="flex flex-col items-center gap-4">
<Spinner />
<p>Processing payment...</p>
</div>
),
dismissOnEscape: false,
dismissOnOutsideClick: false
});
await processPayment();
Dialog.dismiss();Behavior
Stacking
Each Dialog.show() pushes onto a shared overlay stack. The topmost dialog captures Esc and backdrop clicks; lower dialogs ignore them until they become topmost.
Dialogs sit above drawers, so an open dialog is topmost even when a drawer opened after it. A dialog gives up topmost as soon as it starts closing, so the overlay underneath responds to Esc right away instead of waiting for the 300ms exit animation.
Focus Management
Blurs the active element when a dialog opens.
Dismissal
- Escape Key: Closes the topmost dialog by default (disable with
dismissOnEscape: false) - Backdrop Click: Closes the topmost dialog by default (disable with
dismissOnOutsideClick: false). Both the press and the release must land on the backdrop, so dragging to select text past the dialog edge does not close it. - Programmatic:
Dialog.dismiss(id?)from anywhere
Clicks inside the dialog never dismiss it. That includes content rendered in a portal, such as a Select or Menu popup, which lives outside the dialog element in the DOM.
Styling
GeckoUIDialog— Main container (data-state="entering" | "open" | "closing"),z-index: 2000GeckoUIDialog__backdrop— Background overlayGeckoUIDialog__dialog— Dialog content wrapper
Stacked dialogs get an explicit z-index of 2000 + position, so they never depend on DOM order. Drawers start at 1000, which is why dialogs always win.
Accessibility
role="dialog"andaria-modal="true"on the dialog panel- Escape key support
- Backdrop click detection
- A menu, popover or picker open inside it closes first: an outside click or Escape reaches only the topmost overlay, so the next one closes the dialog
Page scroll is locked while a dialog is open, with the scrollbar width paid back as padding so nothing shifts sideways. Stacked overlays share one reference counted lock.
Not handled yet: there is no focus trap, and focus is not restored to the trigger on close. iOS Safari ignores overflow: hidden on the body, so the page behind can still be dragged there.
Related Components
- GeckoUIProvider — Required provider
- ConfirmDialog — Pre-styled confirmation dialogs
- Drawer — Side panel alternative