Dialog (Modal)
- Component overview: Dialog for confirming actions, displaying information, or collecting user input. Also exported as
Modal. - Current status: Supports declarative trigger-as-children usage, config-style content slots, and imperative
Dialog.open()API. - Implementation note:
ui/dialogwraps Radix Dialog primitive; design layer provides config-style API and imperative overlay support. - Default interaction: In the default usage,
childrenis the trigger element.
Basic Usage
Config mode: pass title, content, and use cancelText / okText to render the default cancel / confirm buttons. They auto-close the dialog on click.
render( <div style={{ padding: '40px 0' }}> <Dialog title="Confirm Action" content="Are you sure you want to proceed? This action cannot be undone." cancelText="Cancel" okText="Confirm" onOk={() => console.log('confirmed')} > <Button>Open Dialog</Button> </Dialog> </div>, )
Size
Use size to switch the dialog width. default is 464px; emphasized is 708px, for content-heavy or more prominent dialogs. The two are visually identical apart from width.
render( <div style={{ display: 'flex', gap: 16, padding: '40px 0' }}> <Dialog title="Default (464px)" content="The standard dialog width, suitable for most confirmations." okText="OK" > <Button variant="secondary">Default</Button> </Dialog> <Dialog size="emphasized" title="Emphasized (708px)" content="A wider dialog for content-heavy or more prominent scenarios." okText="OK" > <Button>Emphasized</Button> </Dialog> </div>, )
Hide Close Button
Set showClose={false} to remove the close button.
render( <div style={{ padding: '40px 0' }}> <Dialog title="Important Notice" showClose={false} content="Please read carefully before proceeding." okText="I Understand" > <Button variant="secondary">No Close Button</Button> </Dialog> </div>, )
Destructive Confirm Button
Use okButtonProps to customize the confirm button. Pass variant="destructive" for destructive actions such as deleting data.
render( <div style={{ padding: '40px 0' }}> <Dialog title="Delete Record" content="This action cannot be undone. The record will be permanently deleted." cancelText="Cancel" okText="Delete" okButtonProps={{ variant: 'destructive' }} > <Button variant="destructive-outline">Delete</Button> </Dialog> </div>, )
Content Only
When only content is passed (no title, no buttons), the content fills the dialog area without any padding wrapper — giving you full layout control.
const Demo = () => { const [open, setOpen] = useState(false) return ( <div style={{ padding: '40px 0' }}> <Button variant="secondary" onClick={() => setOpen(true)}>Open Custom Dialog</Button> <Dialog open={open} onOpenChange={setOpen} content={ <div style={{ padding: '32px 24px', textAlign: 'center' }}> <div style={{ fontSize: 40, marginBottom: 12 }}>🎉</div> <div style={{ fontSize: 18, fontWeight: 600, marginBottom: 8 }}>All done!</div> <p style={{ color: '#888', marginBottom: 24 }}>Your changes have been saved successfully.</p> <Button onClick={() => setOpen(false)}>Close</Button> </div> } /> </div> ) } render(<Demo />)
Custom Footer
For more complex layouts, pass footer directly. cancelText / okText are then ignored, and closing logic is up to the caller.
const Demo = () => { const [open, setOpen] = useState(false) return ( <div style={{ padding: '40px 0' }}> <Button onClick={() => setOpen(true)}>Open Dialog</Button> <Dialog open={open} onOpenChange={setOpen} title="Custom Footer" content="With a custom footer, the close action is the caller's responsibility." footer={ <div style={{ display: 'flex', gap: 8, justifyContent: 'space-between', flex: 1 }}> <Button variant="link-color">View Details</Button> <Button onClick={() => setOpen(false)}>Got it</Button> </div> } /> </div> ) } render(<Demo />)
Scrollable Content with Fixed Footer
When content is long, only the content area scrolls — title and footer remain fixed. Scroll within the content area below to verify.
const Demo = () => { const [open, setOpen] = useState(false) return ( <div style={{ padding: '40px 0' }}> <Button onClick={() => setOpen(true)}>Open Long Content Dialog</Button> <Dialog open={open} onOpenChange={setOpen} title="Long Scrollable Content" content={ <div> {Array.from({ length: 25 }, (_, i) => ( <p key={i} style={{ marginBottom: 10 }}> Line {i + 1} — Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. </p> ))} </div> } cancelText="Cancel" okText="Confirm" /> </div> ) } render(<Demo />)
Overlay Inside Dialog (Wheel Scroll)
Popovers and Selects inside a Dialog are portaled to document.body. Wheel scrolling in the dropdown list should work normally — the dialog's scroll lock no longer blocks it.
const Demo = () => { const [open, setOpen] = useState(false) return ( <div style={{ padding: '40px 0' }}> <Button onClick={() => setOpen(true)}>Open Dialog with Select</Button> <Dialog open={open} onOpenChange={setOpen} title="Overlay Inside Dialog" content={ <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <p style={{ margin: 0 }}>The Select below portals its dropdown to document.body. Use the trackpad / wheel to scroll the option list — it should scroll normally.</p> <Select options={Array.from({ length: 40 }, (_, i) => ({ value: String(i), label: `Option ${i + 1}`, }))} placeholder="Select an option" /> </div> } cancelText="Cancel" okText="OK" /> </div> ) } render(<Demo />)
Controlled Mode
const Demo = () => { const [open, setOpen] = useState(false) return ( <div style={{ display: 'flex', gap: 12, alignItems: 'center', padding: '40px 0' }}> <Button onClick={() => setOpen(true)}>Open Controlled</Button> <span style={{ fontSize: 14, color: '#999' }}>open: {String(open)}</span> <Dialog open={open} onOpenChange={setOpen} title="Controlled Dialog" content="This dialog is controlled externally." okText="Close" /> </div> ) } render(<Demo />)
Imperative API
Dialog.open() provides imperative access. Suitable for async callbacks, keyboard shortcuts, or table actions where a JSX trigger is not available. Requires DesignProvider or OverlayHost mounted at the application root.
import { Dialog } from '@plaud/design'
Dialog.open({
title: 'Delete item',
content: <div>This action cannot be undone. Please confirm.</div>,
})
Close from Inside
content and footer support render functions. The function receives a controller with close, update, etc.
Dialog.open({
title: 'Confirm',
content: ({ close }) => (
<div>
<p>Are you sure?</p>
<Button onClick={close}>Close</Button>
</div>
),
})
Update an Open Dialog
open() returns a controller to update configuration after opening:
const controller = Dialog.open({
title: 'Uploading...',
content: <div>Please wait.</div>,
})
setTimeout(() => {
controller.update({
title: 'Upload complete',
content: <div>The file is ready.</div>,
})
}, 1000)
Controller
interface ImperativeOverlayController<TOptions> {
id: string
close: () => void
update: (updater: Partial<TOptions> | ((prev: TOptions) => TOptions)) => void
afterClosed: Promise<void>
}
Props
Dialog (Config Mode)
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | Header title |
content | ReactNode | — | Body content |
size | 'default' | 'emphasized' | 'default' | Dialog width. default = 464px, emphasized = 708px |
cancelText | ReactNode | — | Cancel button label. Auto-closes dialog on click. Ignored when footer is provided |
okText | ReactNode | — | OK / confirm button label. Auto-closes dialog on click. Ignored when footer is provided |
onCancel | () => void | — | Cancel button callback (dialog closes automatically) |
onOk | () => void | — | OK button callback (dialog closes automatically) |
cancelButtonProps | Omit<ButtonProps, 'children'> | — | Extra props for the cancel button, e.g. variant. Ignored when footer is provided |
okButtonProps | Omit<ButtonProps, 'children'> | — | Extra props for the OK button, e.g. variant="destructive". Ignored when footer is provided |
footer | ReactNode | — | Custom footer. When provided, cancelText / okText are ignored and closing is the caller's responsibility |
children | ReactNode | — | Trigger element (non-controlled mode) |
open | boolean | — | Controlled open state |
onOpenChange | (open: boolean) => void | — | Open state change callback |
showClose | boolean | true | Show close button |
destroyOnClose | boolean | true | Unmount content when closed |
contentClassName | string | — | Custom content area class |
contentProps | DialogContentProps | — | Extended props passed to the dialog content layer, such as className, data attributes, and interaction callbacks |
Usage Constraints
- Prefer config-style usage. In the default mode,
childrenacts as the trigger and content is configured through props. - Imperative API requires
DesignProviderorOverlayHostat the application root.