BottomSheet

A modal sheet on a native <dialog> opened with showModal(). The browser puts it in the top layer, makes the page behind inert, keeps focus out of that page and sends Escape as a close request. The ::backdrop is the scrim. Todae adds the scroll lock, the snap points and the drag. Mid and full snap. Drag from the grabber or from the body at scrollTop === 0. List scroll stays free.

The sheet renders where you put it, so it takes the theme of its parent. That also means it takes the parent's native behavior: inside a <form> its inputs submit with that form, and inside <fieldset disabled> its controls are disabled. Render it outside forms, disabled fieldsets and hidden containers such as a closed popover or tab panel. In development, Todae logs an error when a sheet opens somewhere hidden.

Enter slides up from below into the snap height (first-frame pose, not a height tween). Both overlays use data-entered; Dialog also keeps @starting-style. Exit slides off and unmounts on transitionend (240ms fallback). Drag keeps transition: none.

The sheet renders where you place it, with no portal, so it inherits appearance, density and theme overrides like any other part. dismissOnEscape={false} sets closedby="none", so the browser ignores Escape; otherwise Escape and a backdrop click call onOpenChange(false, reason). A controlled sheet closes once you set open to false; an uncontrolled one closes right away. When something outside Todae closes the <dialog>, such as a script calling close(), onOpenChange gets the reason native.

Consumer sketch

Touch Combobox later. A button opens this sheet. Search plus options live in the sheet. Desktop typeahead-while-open stays a popover in the Combobox PRD.

When to use

When: Modal sheet from the bottom (touch Combobox closed control, mobile pickers). Scrim, modal focus, snap mid/full.

When not: Desktop anchored non-modal → Popover. Centered confirm/form → Dialog. Hover tip → Tooltip.

Props

BottomSheet.Root props
NameTypeRequiredDefaultDescription
openbooleanno—Controlled open state. Leave it out for an uncontrolled sheet.
defaultOpenbooleannofalseShows the sheet on mount when uncontrolled. There is no trigger part, so once closed it opens again only on remount.
onOpenChange(open: boolean, reason: SheetCloseReason) => voidno—Dismiss requests. When controlled, staying open keeps the sheet open. reason is "form" for a <form method="dialog"> submit (read returnValue) and "native" when something else outside Todae closed the <dialog>; when controlled, it shows again while open stays true.
snap"mid" | "full"no—Controlled snap. Omit for uncontrolled.
defaultSnap"mid" | "full"no"mid"Initial snap when uncontrolled.
snapPointsReadonlyArray<"mid" | "full">no["mid", "full"]Allowed snaps. ["full"] disables mid.
dismissOnEscapebooleannotrueEscape calls onOpenChange(false, "escape"). false sets closedby="none", so the browser ignores the close request.
dismissOnBackdropbooleannotrueScrim click calls onOpenChange(false, "backdrop").
dismissOnDragbooleannotrueDrag past the threshold dismisses.
aria-labelstringno—Name when BottomSheet.Title is omitted.
BottomSheet.Grabber props
NameTypeRequiredDefaultDescription
aria-labelstringno"Resize sheet"Name of the resize control. Default is the resizeSheet string from StringsProvider.
BottomSheet.Title props
NameTypeRequiredDefaultDescription
childrenReact.ReactNodeno—Title text.
BottomSheet.Close props
NameTypeRequiredDefaultDescription
aria-labelstringno"Close" when its text, not counting aria-hidden or SVG parts, has fewer than two letters or digitsAccessible name. Text of two or more letters or digits, such as "Cancel", names the button by itself. Otherwise the close string from StringsProvider names it, Close in English.
onClickReact.MouseEventHandler<HTMLButtonElement>no—Runs before the close request. Call event.preventDefault() to keep the sheet open.