Detent · grip · content: set them, then open the sheet
Sheet opened 1 time. The drag handle, a scrim tap, or Esc dismisses it (no X by default; flip Close on to add the opt-in X). The island is live.
No X by default (Material canon)
A bottom sheet has no close (X) button by default. The canonical dismissal is the
drag handle (the grabber at the top) plus a scrim tap plus Esc. The X is
opt-in via showClose. Reserve it for the rare case that genuinely needs a persistent,
always-visible dismiss affordance. Every platform follows the same rule (see the Kotlin /
Compose guidance below).
Per the component-vs-view rule, the session
sheets are pure configurations of this sheet + shipped parts; none of them is a
new component. Native cites them as NockerlBottomSheet call-sites already
(AvatarSettingsSheet, SamplingAdvancedSettings); the settings form in the demo
above is exactly this shape:
Dismiss handler: scrim tap, Esc, or (when opted-in) the close button.
title *
string
n/a
Accessible title shown in the header (carries the dialog name).
subtitle
string
n/a
Optional supporting line under the title.
leadingIcon
React.ReactNode
n/a
Optional LEADING ICON shown top-left beside the title (e.g. a settings cog next to "Settings"). A plain functional glyph on the on-plane ink, NOT a status disc, NOT cyan. It aligns to the title line; omit for the default text-only header. The header divider is unchanged either way (explicitly neutral, never cyan).
detent
SheetDetent
'half'
Detent: a half-height peek or a near-full panel.
grip
boolean
true
Show the drag grip at the top.
showClose
boolean
false
OPT-IN close (X) button. Material canon: a bottom sheet has NO X by default. The drag handle + scrim-tap + Esc ARE the dismissal. Set true only when a persistent, always-visible dismiss affordance is genuinely warranted. Default false.
stage *
HTMLElement | null
n/a
The contained stage element that gates rendering so the sheet never escapes it.
footer
React.ReactNode
n/a
PINNED FOOTER-ACTION BAR: an approve/deny row that NEVER scrolls away, rendered below the scrolling body on the sheet's own plane, with a hairline top edge and an upward scroll-under cue when more body remains beneath it. Compose the ratified CTA grammar in here (outline confirm · ghost cancel · destructive outline-red, the Dialog canon). Empty = no footer, byte-identical sheet.
onBack
(() => void) | undefined
n/a
NESTED-SHEET BACK NAVIGATION: when set, the header leads with a back chevron (a real NockerlIconButton) BEFORE the title. View swapping stays with the host (swap title/children on your own state); this is the affordance + a11y.
backLabel
string
'Back'
Accessible name for the back affordance. Default "Back".
children *
React.ReactNode
n/a
Sheet body.
Parameter
Type
Default
Description
onDismissRequest *
() -> Unit
n/a
Invoked when the sheet is dismissed (scrim tap or swipe down).
modifier
Modifier
Modifier
Outer modifier forwarded to the underlying ModalBottomSheet.
The sheet state. Pass skipPartiallyExpanded = false for a half / full partial detent.
contentWindowInsets
@Composable -> WindowInsets
BottomSheetDefaults.windowInsets
Insets applied to the content. Pass WindowInsets(0) for full-bleed.
content *
@Composable ColumnScope.() -> Unit
n/a
Body laid out in the sheet's ColumnScope; the grip + gradient ground are provided by the wrapper.
Cross-platform drift (tracked for reconciliation)
macOS / Swift is out of scope. macOS has no native bottom-sheet idiom. Nockerl
Voice (a menu-bar macOS app) uses windows / popovers (RecordingHUD, InfoTip)
instead. Use a popover / sheet there. This component targets web + Android only.
Detents(resolved): the API name is detent,
chosen to avoid a collision with Compose’s SheetState. Compose (today SheetState +
skipPartiallyExpanded) adopts the shared detent vocabulary.
Grip (drag handle): Compose hosts a custom NockerlSheetGrip inside the body
(dragHandle = null) so the gradient backs it; web renders the bar in CSS. It, not
an X, is the primary dismissal affordance on both platforms.