ResponsiveSheet

A side sheet on desktop, a bottom drawer on small screens.

Usage

import { ResponsiveSheet } from "@voila.dev/ui/responsive-sheet";
<ResponsiveSheet.Root>
	<ResponsiveSheet.Trigger render={<Button variant="outline" />}>Open</ResponsiveSheet.Trigger>
	<ResponsiveSheet.Content size="xl">
		<ResponsiveSheet.Header>
			<ResponsiveSheet.Title>Email details</ResponsiveSheet.Title>
		</ResponsiveSheet.Header>
		<ResponsiveSheet.Body>…</ResponsiveSheet.Body>
	</ResponsiveSheet.Content>
</ResponsiveSheet.Root>

A Sheet on desktop and a bottom Drawer on small screens. Reach for this over plain Sheet for detail panels, which are the ones that hurt most on a phone: a side panel on a narrow viewport is either a sliver or a modal pretending not to be one.

Recipes

size sets the desktop panel width; the mobile drawer is always full-width, because a partial bottom sheet on a phone wastes the only dimension it has.

As with ResponsiveDialog, the root holds the open state so crossing the breakpoint does not close the panel, and autoFocus={false} keeps the keyboard down when the first field is a text input.

API

ResponsiveSheet.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-sheet/components/responsive-sheet-root.tsx.

ResponsiveSheet.Body

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

ResponsiveSheet.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-sheet/components/responsive-sheet-close.tsx.

ResponsiveSheet.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.
sizeSheetContentSize"default"Desktop panel width — the mobile drawer is always full-width.

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

ResponsiveSheet.Description

PropTypeDefault
asChildboolean

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

ResponsiveSheet.Footer

PropTypeDefault
closeLabelstringRenders an outline close button after children.

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

ResponsiveSheet.Header

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

ResponsiveSheet.Title

PropTypeDefault
asChildboolean

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

ResponsiveSheet.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-sheet/components/responsive-sheet-trigger.tsx.

Sheet · ResponsiveDialog · Drawer