Cloud|

@k2b/ui

BottomSheet

Bottom-edge dialogs using the shared modal lifecycle.

BottomSheet@k2b/ui

A bottom-edge sheet with guarded dismissal, plus a short decision that becomes a centered dialog from tablet width. Enter a draft to exercise the discard guard, or resize the open decision.

TSX

Copy
dialogCore.open((close, context) => <BottomSheet onDismiss={context.requestDismiss}>
<BottomSheet.Header title="Details" close={context.requestDismiss} />
<BottomSheet.Body>Content</BottomSheet.Body>
</BottomSheet>, bottomSheetOptions);
// A decision switches in place from sheet to dialog at 48rem.
dialogCore.open((close, context) => (
<BottomSheet onDismiss={context.requestDismiss}>
<BottomSheet.Header title="Apply changes?" />
<BottomSheet.Body>These changes will be visible to everyone in the workspace.</BottomSheet.Body>
<BottomSheet.Footer>
<Button variant="secondary" onClick={() => close()}>Cancel</Button>
<Button onClick={() => close()}>Apply changes</Button>
</BottomSheet.Footer>
</BottomSheet>
), bottomSheetAdaptiveOptions);

Use BottomSheet

BottomSheet is the bottom-edge presentation of PanelDialog. Open it through dialogCore with bottomSheetOptions. It reuses the existing focus trap, Escape and backdrop handling, scroll lock, dialog stack, and dismiss guards.

Sheet or dialog

Menus, pickers, and navigation that belong to the bottom edge on a phone use bottomSheetOptions at every width, or the host opens them only on phones. Decisions, confirmations, and short forms use bottomSheetAdaptiveOptions: a sheet below 48rem and a centered 32rem dialog from 48rem up. CSS chooses the mode, so server and client agree from the first frame and rotation switches the open dialog in place. Dialog mode has no handle; provide a footer action or a header close control.

Wide or fixed-height editors keep panelDialogWideOptions or panelDialogFixedOptions; see PanelDialog. An ending should replace the request and its actions in place without resizing the dialog. Reserve enough space for the ending and move focus to Done.

Import and example

tsx
import { BottomSheet, Button, bottomSheetAdaptiveOptions, dialogCore } from "@k2b/ui";

await dialogCore.open<void>((close, context) => (
  <BottomSheet onDismiss={context.requestDismiss}>
    <BottomSheet.Header title="Details" close={context.requestDismiss} />
    <BottomSheet.Body>Content</BottomSheet.Body>
    <BottomSheet.Footer>
      <Button onClick={() => close()}>Done</Button>
    </BottomSheet.Footer>
  </BottomSheet>
), bottomSheetAdaptiveOptions);

Pass context.requestDismiss to dismissal controls. close(result) completes an operation; it deliberately bypasses cancellation guards. Register unsaved state or pending-operation rules with context.setDismissHandler.

In sheet mode, the handle is a keyboard-accessible button. A downward drag of more than 60 px requests dismissal once. Dragging the body scrolls its content. There are no snap points or second modal lifecycle. dismissDisabled disables the handle; use the shared dismiss guard to protect Escape, backdrop, and header dismissal as well. Set handle={false} to omit the handle, or dismissLabel to override its inherited locale label. tone gives the sheet a dialog tone; on a phone the edge runs along all four sides of the sheet.

Header, Body, Footer, Section, and Tabs use the same props as PanelDialog. The sheet keeps one body scrollport, a bounded viewport height, and a short entrance animation that respects reduced motion. It is also usable on desktop; the host decides when to open it.

The sheet sits on the bottom edge and respects the bottom safe area. When a Footer is the last part of the sheet, the footer carries that inset: its surface reaches the physical screen edge and its actions stay above the home indicator. Otherwise, the sheet pads its content by the inset. Installed web apps need viewport-fit=cover in their viewport meta tag for the inset to apply.

Accessibility

Use a visible header title and meaningful dismiss labels. The native dialog provides modal focus containment; the shared core restores focus on close. Initial focus goes to the first input in the sheet and otherwise moves to the dialog itself, which never draws a ring. The handle never keeps initial focus, even though native showModal() focuses it first, so a sheet that opens without a user gesture (for example from a notification) shows no focus ring. Visible controls stay in the tab order and show a ring on keyboard focus. In adaptive dialog mode, the hidden handle is also removed from the tab order.

Runtime

This is a presentation primitive, not a second dialog manager. dialogCore owns modal state and cancellation. Consumers own pending and dirty state. Focus, Escape, backdrop dismissal, Back with history: true, reduced motion, and safe-area handling apply in both modes. Adaptive dialog mode removes the bottom inset because the frame is clear of the screen edge.

Example

The live draft editor demonstrates guarded dismissal. Type a name and dismiss the sheet to exercise the discard confirmation; Save completes directly.

The live decision example uses bottomSheetAdaptiveOptions. Resize while it is open to switch between a sheet and a centered dialog.

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.