Skip to content
Cloaked Design SystemOverlays

BottomSheet

A mobile bottom-sheet dialog with sheet or full-screen chrome, height presets, swipe-to-dismiss, and optional header and body slots.

Implementation preview
COMPLETE STORYBOOK REFERENCE

Every example. In one place.

Explore every named example, including states, compositions, and theme variants.

12 stories
SELECTED EXAMPLE

Playground

Open controls
Playground selectedOpen canvas
Example sourcepackages/design-system/src/components/overlays/BottomSheet/BottomSheet.stories.tsxExport Playground

Implementation preview Local Storybook build 0.2.0-preview.1. Source specification status does not approve this runtime.

Stories + source JSON ↗

Local specification source status: preview. Implementation preview pending sign-off.

A mobile bottom-sheet dialog with sheet or full-screen chrome, height presets, swipe-to-dismiss, and optional header and body slots.

Design-owned component contract ↗Version 0.2.0-preview.1 · Implementation preview pending sign-off
Implementation API 4 entry points · source-derived preview

Read the actual callable API and preserve its composition constraints. Each union branch is a complete alternative; props marked forbidden cannot be supplied.

import { BottomSheet } from '@cloakedinc/design-system';
BottomSheetBottomSheetBaseProps

packages/design-system/src/components/overlays/BottomSheet/BottomSheet.tsx:104

PropTypePresenceNotes
isOpenbooleanRequiredDrives the sheet. Flipping to `false` starts the exit spring rather than unmounting — the sheet stays in the DOM until that motion settles.
onDismiss() => voidRequiredCalled when the user dismisses (backdrop tap, swipe, or escape).
sizeBottomSheetSizeRequiredHeight preset — `small`/`medium`/`large` are fixed at 40/60/85vh, `full` at 100vh.
lowerednumber | undefinedOptionalSlides the sheet down by this fraction of its own height (0–1) and fades the backdrop in step, without dismissing it.Source default: 0
scrimOpacitynumber | undefinedOptionalBackdrop opacity while the sheet is at full height, clamped to 0-1.Source default: 1
presentationBottomSheetPresentation | undefinedOptionalChrome treatment. `sheet` is rounded bottom-sheet chrome; `fullScreen` fills the viewport.Source default: 'sheet'
surface"page" | "surface" | undefinedOptionalSheet surface token. Defaults to `surface`.Source default: 'surface'
showDragHandleboolean | undefinedOptionalShows the drag handle. Defaults to `true`; it is also the only grab target for `swipeToDismissArea='handle'`.Source default: true
disableSwipeToDismissboolean | undefinedOptionalTurns off swipe-to-dismiss. Defaults to `false`; backdrop tap and Escape still dismiss.Source default: false
disableSwipeToDismissRefReact.RefObject<boolean> | undefinedOptionalSynchronous disable check (same tick) for nested swipe surfaces.
swipeToDismissAreaSwipeToDismissArea | undefinedOptionalRegion where a swipe-to-dismiss gesture can begin. Defaults to the whole sheet.Source default: 'sheet'
ariaLabelstring | undefinedOptionalNames the sheet when it has no `BottomSheet.Title`. A slot is invisible to the compiler, so unlike the old `title` prop this cannot be type-enforced — `@cloakedinc/bottom-sheet-slots` rejects a sheet with neither, and a dev-time check catches what lint cannot read statically.
portalTargetHTMLElement | null | undefinedOptionalMount node for the portal. Defaults to `document.body`.
onBeforeDismiss(() => void) | undefinedOptionalFired right before dismissal completes — host hook for native effects (haptics).
onExited(() => void) | undefinedOptionalFired once the close animation fully settles and the sheet unmounts. Use this to sequence post-dismiss work (e.g. navigation) so it starts only after the sheet has visually left.
hapticHapticFeedback | undefinedOptionalHaptic feedback for dismissal. Defaults to `true` (`light`).Source default: true
classNamestring | undefinedOptional
styleReact.CSSProperties | undefinedOptional
childrenReact.ReactNodeRequired
testIdstring | undefinedOptional`data-testid` for the sheet; the backdrop gets `${testId}-overlay`.
padding0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalPadding on all four sides.
paddingX0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalHorizontal padding (left + right). Overrides `padding` on the X axis.
paddingY0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalVertical padding (top + bottom). Overrides `padding` on the Y axis.
paddingTop0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalTop padding. Overrides `paddingY` and `padding`.
paddingRight0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalRight padding. Overrides `paddingX` and `padding`.
paddingBottom0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalBottom padding. Overrides `paddingY` and `padding`.
paddingLeft0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalLeft padding. Overrides `paddingX` and `padding`.
BottomSheet.HeaderBottomSheetHeaderProps

packages/design-system/src/components/overlays/BottomSheet/BottomSheetHeader.tsx:30

PropTypePresenceNotes
onClose(() => void) | undefinedOptionalShows a close button when provided. Pressing it only fires this callback; actually closing the sheet is the host's job.
childrenReact.ReactNodeOptional
classNamestring | undefinedOptional
styleReact.CSSProperties | undefinedOptional
testIdstring | undefinedOptional`data-testid` for the header; the close button gets `${testId}-close`.
BottomSheet.TitleBottomSheetTitleProps

packages/design-system/src/components/overlays/BottomSheet/BottomSheetTitle.tsx:24

PropTypePresenceNotes
childrenstringRequiredDoubles as the sheet's accessible name.
variantBottomSheetTitleVariant | undefinedOptionalType treatment. Defaults to `sans`.Source default: 'sans'
testIdstring | undefinedOptional`data-testid` for the heading. Rarely needed — the title is not interactable.
BottomSheet.BodyBottomSheetBodyProps

packages/design-system/src/components/overlays/BottomSheet/BottomSheetBody.tsx:28

PropTypePresenceNotes
childrenReact.ReactNodeRequired
scrollboolean | undefinedOptionalScrolls within the slot, with overscroll contained so scrolling doesn't chain to ancestors. Defaults to `true`; `false` defers scrolling to the sheet's own content wrapper.Source default: true
classNamestring | undefinedOptional
styleReact.CSSProperties | undefinedOptional
testIdstring | undefinedOptional`data-testid` for the slot element.
padding0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalPadding on all four sides.
paddingX0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalHorizontal padding (left + right). Overrides `padding` on the X axis.
paddingY0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalVertical padding (top + bottom). Overrides `padding` on the Y axis.
paddingTop0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalTop padding. Overrides `paddingY` and `padding`.
paddingRight0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalRight padding. Overrides `paddingX` and `padding`.
paddingBottom0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalBottom padding. Overrides `paddingY` and `padding`.
paddingLeft0 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 96 | 120 | 240 | undefinedOptionalLeft padding. Overrides `paddingX` and `padding`.

Only explicit destructured parameter initializers are extracted. Other runtime defaults remain in implementation source.

Overview

The mobile bottom-sheet dialog — the standard way to present focused, dismissible content over a screen. It renders through a portal, so keep isOpen in parent state and close it from onDismiss.

Use when

size sets the sheet height. Pick it by how much content the sheet carries.

small, large, auto, and full are all in use today; medium is available but currently unused in the app.

Presentation

presentation defaults to sheet — rounded bottom-sheet chrome with an optional drag handle and swipe-to-dismiss. Switch to fullScreen to fill the viewport edge-to-edge with no radius or handle, for immersive flows. A fullScreen sheet reaches the notch, and BottomSheet.Header is what holds its content clear of it — so give one a header even when the chrome is only a title.

Composition

The sheet is composed entirely from slots. BottomSheet.Header is the top chrome, BottomSheet.Title names the sheet from inside it, and BottomSheet.Body holds the content and owns its own scrolling.

<BottomSheet isOpen={isOpen} onDismiss={close} size='large' testId='privacy-detail-sheet'>
    <BottomSheet.Header onClose={close} testId='privacy-detail-sheet-header'>
        <BottomSheet.Title>Privacy detail</BottomSheet.Title>
        <Text variant='body-small' color='secondary'>
            Updated today
        </Text>
    </BottomSheet.Header>
    <BottomSheet.Body>
        <Text variant='body' color='secondary' as='p'>
            Composed slots keep titles, close actions, and body spacing consistent.
        </Text>
    </BottomSheet.Body>
</BottomSheet>

BottomSheet.Title must be a direct child of BottomSheet.Header. A fragment between the two is fine; a wrapper element is not, and @cloakedinc/bottom-sheet-slots fails the lint if you reach for one. The reason is the fullScreen presentation: .header is the single element that carries the sheet's top safe-area inset, and a title that could also sit topmost gives that inset two possible owners — the padding bug this containment closes.

The title shares a row with the close button, and every other header child stacks below that row. It draws no divider of its own; if a rule between header and body is what the design calls for, place a Divider yourself.

variant='serif' swaps the title to the display serif. The default is sans.

Accessible name

The title doubles as the sheet's accessible name, so most sheets need nothing further. A sheet with no title — a picker whose options speak for themselves — takes ariaLabel instead. A slot is invisible to the compiler, so this is the one requirement the types cannot enforce. @cloakedinc/bottom-sheet-slots fails the lint on a sheet with neither, and a sheet whose title only appears at runtime logs a dev-time error — pass ariaLabel there, since a Title that does mount takes the name back.

Required decisions

disableSwipeToDismiss forces a deliberate choice — the sheet closes only via the backdrop or an in-content action, not a swipe.

Usage by variant

ChoiceUse whenAvoid when
Size: smallBrief content — a short option list, a confirm, an info blurb. The most common size in the app.Long or scrolling content that will feel cramped.
Size: mediumA moderate amount of content that needs more room than a small sheet.
Size: largeTaller content — a catalog, results, or a detail with several rows.
Size: fullLong or scrolling content that should still read as a bottom sheet; reaches the sheet max-height, stopping below the status bar.
Size: autoContent whose height you want the sheet to hug exactly, capped at the sheet max-height.Content whose height varies enough that a fixed preset reads more predictably.

Component specification

$schema../../../specs/component-spec.schema.json
Parts
Id
root
Label
Root
Description
The sheet surface presenting the dialog.
Required
true
Id
overlay
Label
Overlay
Description
Backdrop that dims and dismisses on tap.
Id
drag-handle
Label
Drag handle
Description
Optional grab affordance at the top of the sheet.
Id
content
Label
Content
Description
Scrollable content region.
Id
header
Label
BottomSheet.Header
Description
Top chrome slot: an optional BottomSheet.Title, an optional close button, and any content beneath them.
Id
title
Label
BottomSheet.Title
Description
Heading slot that names the sheet. Belongs inside BottomSheet.Header.
Id
body
Label
BottomSheet.Body
Description
Composed body slot with consistent spacing.
Allowed Design Choices
Variants
Sheet
Label
Sheet
Description
Rounded bottom-sheet chrome with an optional drag handle.
Full screen
Label
Full screen
Description
The fullScreen presentation fills the viewport with no radius.
Sizes
Small
Label
Small
Description
40vh height preset.
Medium
Label
Medium
Description
60vh height preset.
Large
Label
Large
Description
85vh height preset.
Full
Label
Full
Description
Fills the viewport height.
Auto
Label
Auto
Description
Hugs content height, capped at the sheet max-height.
Variants
Id
sheet
Label
Sheet
Description
Rounded bottom-sheet presentation.
Platforms
  • ios
  • android
  • web
Id
full-screen
Label
Full screen
Description
Edge-to-edge fullScreen presentation.
Platforms
  • ios
  • android
  • web
Sizes
Id
small
Label
Small
Description
40vh sheet.
Id
medium
Label
Medium
Description
60vh sheet.
Id
large
Label
Large
Description
85vh sheet.
Id
full
Label
Full
Description
Full-viewport sheet.
Id
auto
Label
Auto
Description
Content-height sheet.
Measurements
Id
sheet-padding-top
Label
Sheet top inset
Kind
exact
Token
--cds-space-24
Value
24
Unit
px
Applies To
  • root
Id
sheet-inline-inset
Label
Content horizontal gutter
Kind
exact
Token
--cds-space-20
Value
20
Unit
px
Applies To
  • content
Notes
Default gutter on the content region; padding props on the sheet override it, so paddingX={0} hosts edge-to-edge content. The built-in title keeps this inset either way.
Id
content-gap
Label
Content gap
Kind
exact
Token
--cds-space-12
Value
12
Unit
px
Applies To
  • content
Id
corner-radius
Label
Sheet corner radius
Kind
exact
Token
--cds-radius-xl
Value
24
Unit
px
Applies To
  • root
Notes
Top corners only; the sheet presentation rounds its top edge.
Layout
Alignment
The sheet anchors to the bottom edge and centers horizontally; content scrolls within it.
Flow
A backdrop hosts the sheet, which stacks an optional drag handle, an optional header slot, and scrollable content.
Notes
  • Compose BottomSheet.Header with a BottomSheet.Title inside it; there is no title prop on the root.
  • BottomSheet.Title must sit inside BottomSheet.Header. The header is the one element reserving the top safe-area inset, so a title that could also be topmost would need the inset in two places.
  • The top safe-area inset is reserved only by the fullScreen presentation, which reads --cds-safe-area-top-chrome; a bottom-anchored sheet never reaches the notch.
  • Padding props on the sheet set the content gutter and default to the 20px page inset; paddingX={0} lets a child run edge to edge.
  • BottomSheet.Header keeps the 20px gutter whatever the padding props say, so paddingX={0} opens up the body without dragging the title to the screen edge.
Typography And Color
Typography
Label
Title
Description
BottomSheet.Title uses the h4 ramp, or the h3-serif ramp with variant="serif".
Token
--cds-type-h4-size
Colors
Label
Surface
Description
The sheet uses the raised surface by default.
Token
--cds-bg-surface
Label
Content text
Description
Content uses primary text.
Token
--cds-text-primary
States
Id
open
Label
Open
Description
Mounted and presented over the backdrop.
Id
dismissing
Label
Dismissing
Description
Sliding out after a dismiss gesture or backdrop tap.
Id
lowered
Label
Lowered
Description
Slid partway down with the scrim faded, revealing the screen behind without dismissing.
Id
swipe-locked
Label
Swipe locked
Description
Swipe-to-dismiss disabled for required decisions.
Behavior
Label
Controlled open
Description
Keep isOpen in parent state and close from onDismiss.
Label
Dismissal
Description
Backdrop tap, swipe down, or Escape dismiss the sheet unless swipe is disabled.
Label
Lowering
Description
A host can set lowered (0-1) to duck the sheet by that fraction of its height and bring it back; the scrim fades in step and the sheet stays mounted and controlled.
Label
Scrim opacity
Description
scrimOpacity (0-1) caps how dark the backdrop may go, for a host that needs the screen behind the sheet readable while the sheet holds still. It composes with lowered by relief, not by multiplication: the scrim renders at whichever of the two asks for more, so a sheet that only ducks behaves exactly as it did before.
Accessibility
Label
Dialog semantics
Description
The sheet is role=dialog with aria-modal, named by its BottomSheet.Title when one is present and by ariaLabel otherwise. A slot is invisible to the compiler, so a sheet with neither is caught by a dev-time check rather than a type error.
Label
Focus management
Description
Focus moves into the sheet once the entrance settles, and is restored to the launcher the moment a close becomes irreversible — a dismiss during the entrance cancels the pending move rather than pulling VoiceOver into a sheet that is already leaving. The sheet suppresses the focus outline: the runtime is touch-only, so a ring on its edge signals nothing.
Content Adaptation
Label
Height presets
Description
size sets the sheet height; auto hugs content up to the max-height.
Label
Keyboard avoidance
Description
The sheet insets its bottom padding when the keyboard is shown.
Label
Content gutter
Description
The content region carries the page gutter by default. Pass paddingX={0} for edge-to-edge content such as a full-bleed list, and inset the sheet's own chrome instead.
Motion Haptics
Motion
Label
Slide and spring
Description
The sheet slides up on open and spring-tracks the drag on dismiss.
Label
Lower and return
Description
Lowering animates over 400ms with --cds-ease-in-out, on a separate property from the dismiss spring so the two never fight.
Haptics
Label
Dismiss haptic
Description
Dismissal requests a light haptic before it completes.
Platform Differences
Platforms
  • web
Description
Web renders the sheet through a portal to document.body and supports Escape-to-dismiss for hardware keyboards.
Rationale
The bottom-sheet interaction is the cross-platform product pattern; the portal and Escape support keep it usable in the web runtime.
Implementation preview

This design-owned implementation is ready to try and review. It does not establish product approval. Imported specifications retain their original review status.

Inspect version 0.2.0-preview.1