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
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| open | boolean | no | — | Controlled open state. Leave it out for an uncontrolled sheet. |
| defaultOpen | boolean | no | false | Shows 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) => void | no | — | 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. |
| snapPoints | ReadonlyArray<"mid" | "full"> | no | ["mid", "full"] | Allowed snaps. ["full"] disables mid. |
| dismissOnEscape | boolean | no | true | Escape calls onOpenChange(false, "escape"). false sets closedby="none", so the browser ignores the close request. |
| dismissOnBackdrop | boolean | no | true | Scrim click calls onOpenChange(false, "backdrop"). |
| dismissOnDrag | boolean | no | true | Drag past the threshold dismisses. |
| aria-label | string | no | — | Name when BottomSheet.Title is omitted. |
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| aria-label | string | no | "Resize sheet" | Name of the resize control. Default is the resizeSheet string from StringsProvider. |
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| children | React.ReactNode | no | — | Title text. |
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| aria-label | string | no | "Close" when its text, not counting aria-hidden or SVG parts, has fewer than two letters or digits | Accessible 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. |
| onClick | React.MouseEventHandler<HTMLButtonElement> | no | — | Runs before the close request. Call event.preventDefault() to keep the sheet open. |