ResponsiveDialog

A dialog on desktop, a bottom drawer on small screens.

Usage

import { ResponsiveDialog } from "@voila.dev/ui/responsive-dialog";
<ResponsiveDialog.Root>
	<ResponsiveDialog.Trigger render={<Button variant="outline" />}>Open</ResponsiveDialog.Trigger>
	<ResponsiveDialog.Content>
		<ResponsiveDialog.Header>
			<ResponsiveDialog.Title>Cancel this project?</ResponsiveDialog.Title>
		</ResponsiveDialog.Header>
		<ResponsiveDialog.Body>…</ResponsiveDialog.Body>
		<ResponsiveDialog.Footer>
			<Button variant="destructive">Cancel project</Button>
		</ResponsiveDialog.Footer>
	</ResponsiveDialog.Content>
</ResponsiveDialog.Root>

A Dialog on desktop and a bottom Drawer below 768px, behind one set of parts. A centred modal on a phone is the wrong shape — it fights the keyboard and puts its buttons where thumbs are not — so this switches rather than scaling. Use it for anything a phone user will meet; keep plain Dialog for desktop-only surfaces like an admin console.

Recipes

The switch happens at 768px. Open the preview above and narrow the window to see it.

The root owns the open state deliberately: the two halves are different component trees, so uncontrolled state would reset when the viewport crosses the breakpoint mid-interaction.

autoFocus={false} is worth knowing about when the first field is a text input. Focusing it on open raises the keyboard over the content the user just asked to see; the content stays reachable by tab either way.

API

ResponsiveDialog.Root

PropTypeDefault
childrenReactNodeThe trigger and the content.
defaultOpenbooleanfalseWhether it starts open, when uncontrolled.
onOpenChange((open: boolean) => void)Called when the overlay opens or closes, on either surface.
openbooleanControlled open state. The root owns it so it survives crossing the breakpoint.

Plus the DOM props of the element it renders. Source: responsive-dialog/components/responsive-dialog-root.tsx.

ResponsiveDialog.Body

Plus every <div> prop. Source: responsive-dialog/components/responsive-dialog-body.tsx.

ResponsiveDialog.Close

PropTypeDefault
renderReactElement<unknown, string | JSXElementConstructor<any>>Element form only — the drawer half clones it, so render functions are unsupported.

Plus the DOM props of the element it renders. Source: responsive-dialog/components/responsive-dialog-close.tsx.

ResponsiveDialog.Content

PropTypeDefault
autoFocusbooleantrueWhether opening moves focus into the content (the default). Pass false when the first field is a text input you don't want to focus on open — on a phone that focus opens the keyboard over the content you just asked to see. The content stays reachable by tab either way.
childrenReactNodeHeader, body and footer parts.
classNamestringClasses for the panel on both surfaces.
closeButtonLabelstring"Close"Accessible name for that X. This package ships no translations.
overlayClassNamestringClasses for the backdrop behind the panel. className styles the panel itself.
showCloseButtonbooleantrueDraws the X in the corner.
sizeDialogContentSize"sm"Desktop dialog width — the mobile drawer is always full-width.

Plus the DOM props of the element it renders. Source: responsive-dialog/components/responsive-dialog-content.tsx.

ResponsiveDialog.Description

PropTypeDefault
asChildboolean

Plus the DOM props of the element it renders. Source: responsive-dialog/components/responsive-dialog-description.tsx.

ResponsiveDialog.Footer

PropTypeDefault
closeLabelstringText of the built-in dismiss button. Omit the prop to render no dismiss at all.

Plus every <div> prop. Source: responsive-dialog/components/responsive-dialog-footer.tsx.

ResponsiveDialog.Header

Plus every <div> prop. Source: responsive-dialog/components/responsive-dialog-header.tsx.

ResponsiveDialog.Title

PropTypeDefault
asChildboolean

Plus the DOM props of the element it renders. Source: responsive-dialog/components/responsive-dialog-title.tsx.

ResponsiveDialog.Trigger

PropTypeDefault
renderReactElement<unknown, string | JSXElementConstructor<any>>Element form only — the drawer half clones it, so render functions are unsupported.

Plus the DOM props of the element it renders. Source: responsive-dialog/components/responsive-dialog-trigger.tsx.

Dialog · ResponsiveSheet · Drawer