East Blue
KeepNeeds Refinement
Bottom Sheet Component link

The bottom-anchored sheet surface — a drag handle, a slotted header, an optional subtitle, and content and footer slots a consumer fills.

Keep — all findings resolved
Rebuilt on node 5304:32717 in the 2026 Working File. The component is the sheet rather than its header — DragHandle → Header → Description → Content-Slot → Footer-Slot, with five real Figma Slots and a vector Close instance. Naming is complete: TitleAlignment and FooterOrientation follow §1 and §5, the subtitle is a single Subtitle enum enforcing an exclusivity that was previously an unwritten convention, slots are kebab-cased per §4, and text layers follow §7 as Preamble → Title → Description. The centred header is confirmed a deliberate second layout rather than a stripped-down first one, the present/dismiss/detent contract is documented and owner-confirmed, and the boundary with Modal and Overlay is recorded — Overlay is the scrim, Bottom Sheet is bottom-anchored and gesture-dismissible, Modal is centred and blocking. All four DS Health traits pass; the only item still open is Code Connect, blocked until the native library exists.
In Context

Bottom Sheet anchors to the bottom edge over a dimmed background. In the sticker-sheet context file (12522:109042), instances are used across a wide range of content shapes: ID pickers, confirmation dialogs, transfer summaries, tips lists, welcome cards, and switch-account prompts — each with different inner composition, all wrapped in the same surface.

Live Preview

Preamble here...

Title here of the header...

This is a body description

Properties
TitleAlignment
FooterOrientation
hasSupportingText
hasDescription
Content
Preamble
Title
Description
Supporting text
DS Health
Reusable
Pass
A list picker, a confirmation, a form and a tips list are now the same component with different slot contents. The content slot removed the reason every product usage previously detached or spawned a local variant.
Self-contained
Pass
Owns its surface, drag handle, header structure and subtitle. It correctly does not own the scrim — that belongs to the platform presentation on both iOS and Android — though that division still needs stating for consumers.
Consistent
Pass
TitleAlignment and FooterOrientation are PascalCase per §1 with Title Case values per §5, Subtitle collapses two mutually exclusive booleans into one enum so the type no longer permits an undefined combination, the five slots are kebab-cased per §4, frames are PascalCase, and text layers follow the §7 hierarchy as Preamble → Title → Description. The centred header carrying fewer slots is recorded as a deliberate second layout rather than an inconsistency.
Composable
Pass
Five real Figma Slots — content, footer, and three in the header — so body, actions, leading icon and trailing control are all consumer-supplied. The footer buttons are slot defaults rather than fixed structure.
Behavior
State iOS Android Figma Property Notes
Present / dismiss Yes Yes Not annotated iOS: .sheet(isPresented:). Android: ModalBottomSheet(onDismissRequest:). Slide-up entrance implied by pattern, not documented on the component.
Drag handle (grabber) Yes Yes Missing No handle node in Figma. iOS renders via .presentationDragIndicator(.visible). Material 3 renders via ModalBottomSheet(dragHandle = { BottomSheetDefaults.DragHandle() }).
Detent snapping (medium / large) Yes Yes Missing Sheet height in Figma is driven by content height only — no half / full axis. Natively handled via .presentationDetents([.medium, .large]) / SheetValue.PartiallyExpanded.
Swipe-down-to-dismiss Yes Yes Not annotated Platform-native gesture — should be configurable via a dismissible boolean on the wrapper.
Scrim / tap-outside dismiss Yes Yes Not composed Scrim lives in the separate Overlay component today (47:329691). Sheet should consume it, not redraw.
Close button (X) Yes Yes Asymmetric Only present on Left Align. Raster PNG. Should become a trailing slot in a title-bar region, available to both alignments.
Header slot (e.g. stepper) Yes Yes Center Align only Center Align silently adds a headerSlot used for progress bars / steppers. Left Align has no equivalent. Either surface it on both or model as its own region.
Content scroll lock N/A N/A Not documented Background scroll locked while sheet is presented; sheet's own content scrolls independently when detent < content height.
Empty / loading / error (content) Yes Yes Not modeled Content slot owner's responsibility; sheet itself has no intrinsic empty/loading/error state.
Issues
  • Scope corrected — the component is now the sheet, not its header. v2.0: Rebuilt on node 5304:32717 in the 2026 Working File. The shell is dragHandle → header → description → content → footer, so a list picker, a confirmation and a form are all the same component with different slot contents. This was the headline finding of the previous assessment and it is fully addressed. (C4 · Composition) C4 · Native Mappability
  • Content region is a real Figma Slot. v2.0: The four decorative placeholder rectangles are gone. content is a genuine SLOT node, so a consumer drops their own body in without detaching and without spawning a product-local variant. This is what took Reusable and Composable from fail to pass. (C1 · Slot) C1 · Layer Structure & Naming
  • Footer is a slot, not baked buttons. v2.0: footer is a SLOT carrying two Button - Large/Medium instances as default content rather than as fixed structure. Action count and pairing are now the consumer’s decision, and the footerOreintation axis switches the default pair between a 312px stack and two 150px side-by-side buttons. (C1 · Slot) C1 · Layer Structure & Naming
  • Header gained three named slots. v2.0: aboveTitleSlot, leadingSlot and trailingSlot are all real SLOT nodes around a titleBlock. The raw grey circle that stood in for a leading icon and the baked close control are both replaced by slots a consumer fills. (C1 · Slot) C1 · Layer Structure & Naming
  • Drag handle is a shared primitive. v2.0: dragHandle is an instance wrapping a 32×4 #C2CFE5 pill at radius 99999, consistent across all eight variants. The sheet no longer redraws its own affordance. (C6 · Composition) C6 · Asset & Icon Quality
  • Component renamed to Bottom Sheet. v2.0: The set now reads Bottom Sheet rather than Bottom Drawer, matching how the pattern is actually referred to and how both platforms name it. The token namespace was not readable through the review tooling and is tracked separately. (C1 · Rename) C1 · Layer Structure & Naming
  • footerOreintation typo corrected. v2.1: Verified on the live node — all eight variants now read footerOrientation. Fixed before Code Connect or any generated native constant could bind to the misspelling, which is the cheap moment to do it. (C2 · Rename) C2 · Variant & Property Naming
  • Variant values moved to Title Case. v2.1: Left | Center, Vertical | Horizontal and True | False across all eight variants, per §5. (C2 · Rename) C2 · Variant & Property Naming
  • Slots kebab-cased and frames moved to PascalCase. v2.1: content → Content-Slot, footer → Footer-Slot, aboveTitleSlot → Title-Slot, leadingSlot → Leading-Slot, trailingSlot → Trailing-Slot, all kebab-case per §4. The wrapping frames header and description are now Header and Description, and #description → Description, dropping the last legacy sigil. (C1 · Rename) C1 · Layer Structure & Naming
  • Header slots now carry real content. v2.1: Leading-Slot holds a Placeholder instance as its swap target and Trailing-Slot holds a Close icon instance built on a shape_full boolean operation — the raster close control the original assessment flagged is gone, replaced by a vector from the shared library. (C6 · Asset) C6 · Asset & Icon Quality
  • Title-Slot renamed Above-Title-Slot. v2.2: Position meaning restored. The slot sits above the title row for a badge, an illustration or an eyebrow, and its name now says so rather than implying it holds the title. (C1 · Rename) C1 · Layer Structure & Naming
  • Title text layer renamed Title. v2.2: 5377:35197 was called Header inside a frame also called Header — two things under one name in a single variant. It now reads Title, which resolves the collision and completes the §7 hierarchy alongside its sibling Preamble. (C1 · Rename) C1 · Layer Structure & Naming
  • Drag Handle → DragHandle. v2.2: The space is gone, matching the unspaced PascalCase used for frames everywhere else in the system. (C1 · Rename) C1 · Layer Structure & Naming
  • Naming pass complete. v2.3: Verified on the live node. The ⤷ glyph is stripped from all three header slots, so the five slots now read one way — Above-Title-Slot, Leading-Slot, Trailing-Slot, Content-Slot, Footer-Slot, kebab-case per §4. titleRow and titleBlock are now TitleRow and TitleBlock, and the enum properties read TitleAlignment and FooterOrientation in PascalCase per §1 with Title Case values per §5. The two booleans correctly stay lowerCamelCase per §2. Nothing in the component carries a legacy name, a sigil or a decorative character. (C1 · C2 · Rename) C1 · Layer Structure & Naming
  • Centred header confirmed control-free. v2.4: Confirmed by the owner as a deliberate constraint. TitleAlignment=Center drops Leading-Slot and Trailing-Slot because a centred title serves a different kind of sheet — a confirmation, a success or a celebration, where the whole surface is the message and the footer carries every action. A close control in the corner of that layout competes with the centred composition and duplicates a dismissal the footer already offers. A sheet that needs a leading icon or a close button uses TitleAlignment=Left, which carries both slots. Native implementations should treat the two alignments as two header layouts rather than one layout with a text-align flag. Recorded so the missing slots read as a rule rather than as an incomplete variant. (C2 · Property) C2 · Variant & Property Naming
  • Present, dismiss and detent contract documented. v2.4: Height — the sheet is content-sized: it grows to fit Content-Slot and stops at a maximum of roughly 90% of the viewport, beyond which the content scrolls inside the sheet while the header and footer stay pinned. There is no fixed detent ladder, which is why no detent axis exists in Figma. Dismissal — the DragHandle is functional: swipe-down dismisses, and tapping the scrim dismisses. Where a flow must not be abandoned midway, the consumer disables both and relies on the footer actions. Scrim — supplied by the platform presentation, not by this component, which is why the set has no scrim layer. Native mapping — iOS .sheet with .presentationDetents([.height(contentHeight), .large]), .presentationDragIndicator(.visible) and .interactiveDismissDisabled() where dismissal is blocked; Android ModalBottomSheet with sheetState, dragHandle = { BottomSheetDefaults.DragHandle() } and properties = ModalBottomSheetProperties(shouldDismissOnBackPress = …). Confirmed by the owner as the intended contract rather than an inference, so implementations can treat it as binding. (C5 · Docs) C5 · Interaction State Coverage
  • Boundary with Modal and Overlay recorded. v2.4: The three are not duplicates once their jobs are named. Overlay is the scrim itself — a full-viewport dimming layer in three strength tiers, and nothing else; it is what a surface sits on top of. Bottom Sheet is bottom-anchored, content-sized, draggable and dismissible, for choices and flows where the underlying screen stays relevant — pickers, summaries, forms. Modal is centre-anchored and blocking, for confirmations that must be answered before anything else continues; it does not drag and should not be dismissible by scrim tap. The rule of thumb: bottom-anchored and dismissible by gesture is a Bottom Sheet, centred and requiring an answer is a Modal, and the dimming behind either is Overlay. Bottom Sheet does not contain a scrim because the platform presentation supplies it; Modal, which is placed rather than presented, composes Overlay directly. (C4 · Family) C4 · Native Mappability
  • Subtitle modelled as one enum. v2.5: hasSupportingText × hasDescription is replaced by Subtitle = None | Supporting | Description. The two booleans were never independent — no variant in the set carries both, because they are alternative treatments of the same slot beneath the title — so the pair advertised a fourth combination the component does not define, with nothing preventing a designer landing on it. The enum enforces the exclusivity in the type rather than as an unwritten convention, and makes the matrix honest at 3 × 2 × 2 = 12 rather than a nominal 16. Accepted as a breaking property change, taken now while the cost is a reset binding in Figma rather than a native API revision after Code Connect exists. Remaining gap, now countable: Center ships only the Description subtitle, so four of the twelve are unbuilt pending the same call already made about the centred header’s slots. (C2 · Property) C2 · Variant & Property Naming
  • Centred sheets take the Description subtitle only — matrix complete at 8. v2.6: Confirmed by the owner — the centred-header rule extends to the subtitle. Center is the layout for confirmations, successes and celebrations, where the surface itself is the message — so it takes the fuller Description treatment, while Supporting is the denser, left-aligned option and None belongs to sheets whose content carries the meaning. The four Center × Supporting|None combinations are therefore unsupported rather than unbuilt, and eight is the complete matrix: Left × 3 subtitles × 2 footer orientations = 6, plus Center × Description × 2 = 2. Native implementations should treat a centred sheet without a description as out of contract. Recorded so the gap reads as the same rule that governs the centred header’s slots rather than as a backlog. (C2 · Property) C2 · Variant & Property Naming
Types
Left

Preamble here...

Title here of the header...

This is a body description

Properties
FooterOrientation
hasSupportingText
hasDescription
Properties
TitleAlignment Left
FooterOrientation Vertical
hasSupportingText False
hasDescription True
Colors
Surface #FFFFFF
Drag handle #C2CFE5
Preamble #90A8D0
Title #0A2757
Supporting text #445C85
Description #445C85
Primary button #005CE5
Primary label #FFFFFF
Tertiary label #005CE5
Leading placeholder #D7E0EF
Close #6780A9 @ 80%
Typography
Preamble Primary/Label/Small
Title Primary/Headlines/Section
Message Secondary/Bold/Base
Description Secondary/Default/Base
Layout
Height 404
Width 360
Radius 0
Padding H —
Padding V —
Gap —
Border 0
Alignment —
Colors by TitleAlignment
Role Element Token Value
Left Surface — #FFFFFF
Left Drag handle — #C2CFE5
Left Preamble — #90A8D0
Left Title — #0A2757
Left Supporting text — #445C85
Left Description — #445C85
Left Primary button — #005CE5
Left Primary label — #FFFFFF
Left Tertiary label — #005CE5
Left Leading placeholder — #D7E0EF
Left Close — #6780A9 @ 80%
Center Surface — #FFFFFF
Center Drag handle — #C2CFE5
Center Preamble — #90A8D0
Center Title — #0A2757
Center Description — #445C85
Center Primary button — #005CE5
Center Primary label — #FFFFFF
Center Tertiary label — #005CE5
Center

Preamble here...

Title here of the header...

This is description

Properties
FooterOrientation
Properties
TitleAlignment Center
FooterOrientation Vertical
hasSupportingText False
hasDescription True
Colors
Surface #FFFFFF
Drag handle #C2CFE5
Preamble #90A8D0
Title #0A2757
Description #445C85
Primary button #005CE5
Primary label #FFFFFF
Tertiary label #005CE5
Typography
Preamble Primary/Label/Small
Title Primary/Headlines/Section
Description Secondary/Default/Base
Layout
Height 378
Width 360
Radius 0
Padding H —
Padding V —
Gap —
Border 0
Alignment —
Installation Planned API

iOS — Swift Package Manager

.package(url: "https://github.com/AY-Org/eb-ds-ios", from: "1.0.0")

Android — Gradle (Kotlin DSL)

implementation("com.eastblue.ds:bottom-sheet:1.0.0")

Import

import EastBlueDS
import com.eastblue.ds.bottomsheet.*
Property Mapping

TitleAlignment, FooterOrientation, hasSupportingText and hasDescription are the set’s four variant properties; Center builds only with hasDescription (v2.x decision). Above-Title-Slot, Leading-Slot, Trailing-Slot, Content-Slot and Footer-Slot are Figma Slots. Preamble, Title, Message and Description are text layers read from 5304:32718 and 5377:35367 — their exposure as text properties is not readable with the plugin.

Figma PropertySwiftUICompose
TitleAlignment — Left, Center .ebTitleAlignment(.leading / .center) titleAlignment = EBTitleAlignment.Leading / Center
FooterOrientation — Vertical, Horizontal .ebFooter(.vertical / .horizontal) { … } footerOrientation = EBFooterOrientation.Vertical / Horizontal
hasSupportingText — True, False .ebSupportingText("…") when True (Left only) supportingText = "…" or null
hasDescription — True, False EBSheetDescription("…") when True description = "…" or null
Above-Title-Slot (slot) .ebAboveTitle { EBStepper(…) } aboveTitle = { EBStepper(…) }
Leading-Slot (slot) .ebLeading { Image("…") } leading = { Icon(…) }
Trailing-Slot (slot) .ebTrailing { EBCloseButton() } — Close by default trailing = { EBCloseButton() } — Close by default
Content-Slot (slot) content: () -> Content content = { … }
Footer-Slot (slot) .ebFooter(…) { EBButton(…); EBTextButton(…) } footer = { EBButton(…); EBTextButton(…) }
Preamble (text) preamble: String? preamble = "…"
Title (text) EBSheetHeader("…") title = "…"
Message (text) .ebSupportingText(_:) supportingText = "…"
Description (text) EBSheetDescription(_:) description = "…"
Usage Snippets Planned API
Left
EBBottomSheet(isPresented: $show) {
    EBSheetHeader("Title here of the header...", preamble: "Preamble here...")
    EBSheetDescription("This is a body description")
    content
}
    .ebTitleAlignment(.leading)
    .ebFooter(.vertical) {
        EBButton("Label") { }
        EBTextButton("Label") { }
    }
EBBottomSheet(
    title = "Title here of the header...",
    preamble = "Preamble here...",
    description = "This is a body description",
    titleAlignment = EBTitleAlignment.Leading,
    footerOrientation = EBFooterOrientation.Vertical,
    footer = { EBButton("Label") { }; EBTextButton("Label") { } },
    content = { … }
)
Center
EBBottomSheet(isPresented: $show) {
    EBSheetHeader("Title here of the header...", preamble: "Preamble here...")
    EBSheetDescription("This is a body description")
    content
}
    .ebTitleAlignment(.center)
    .ebFooter(.vertical) {
        EBButton("Label") { }
        EBTextButton("Label") { }
    }
EBBottomSheet(
    title = "Title here of the header...",
    preamble = "Preamble here...",
    description = "This is a body description",
    titleAlignment = EBTitleAlignment.Center,
    footerOrientation = EBFooterOrientation.Vertical,
    footer = { EBButton("Label") { }; EBTextButton("Label") { } },
    content = { … }
)
Accessibility
RequirementiOSAndroid
Modal semantics Present with .sheet or .presentationDetents; VoiceOver treats the sheet as modal and focus moves to the Title. ModalBottomSheet — TalkBack scopes to the sheet; request focus on the Title.
Title as heading .accessibilityAddTraits(.isHeader) on the Title; Preamble reads before it. Modifier.semantics { heading() } on the Title.
Drag handle .presentationDragIndicator(.visible); announce "swipe down to dismiss". BottomSheetDefaults.DragHandle with contentDescription = "Swipe down to dismiss".
Close and escape Trailing Close is a Button labelled "Close"; .accessibilityAction(.escape) dismisses. IconButton labelled "Close"; back press dismisses.
Footer actions Primary first, then Tertiary, in reading order whatever the orientation. Same order; both are focusable buttons.
Reduce motion Cross-fade instead of slide when Reduce Motion is on. Respect Settings.Global.ANIMATOR_DURATION_SCALE; cross-fade when 0.
Usage Guidelines

Do

Use Left for sheets with a leading icon or a close button; Center for short confirmations.

Don't

Don’t use Center with supporting text — the set does not build it.

Do

Use hasSupportingText for one line under the title; hasDescription for a body paragraph.

Don't

Don’t turn both on in one sheet — the set builds one or the other.

Do

Use Horizontal footers for equal-weight choices; Vertical when one action leads.

Don't

Don’t put two primary buttons in the footer.

Do

Put scrolling content inside Content-Slot only.

Don't

Don’t let the header or footer scroll away.

Criteria Scorecard
ID Criterion Status Notes
C1 Layer Structure & Naming Ready DragHandle › Header › Above-Title-Slot / TitleRow › Leading-Slot / TitleBlock / Trailing-Slot › Description › Content-Slot › Footer-Slot — read on 5304:32718.
C2 Variant & Property Naming Ready TitleAlignment, FooterOrientation with Title Case values; hasSupportingText and hasDescription are real booleans.
C3 Token Coverage Ready Preamble → Primary/Label/Small, Title → Primary/Headlines/Section, Message → Secondary/Bold/Base, Description → Secondary/Default/Base. Colour variable bindings are not readable with the plugin — confirm in Dev Mode.
C4 Native Mappability Ready Two enums, two booleans, five slots, four strings — maps onto EBBottomSheet over the platform sheet primitives.
C5 Interaction State Coverage Ready Present, dismiss and detent contract documented (v2.x); the buttons and Close take their states from their own components.
C6 Asset & Icon Quality Needs Refinement DragHandle is the shared primitive and the leading Placeholder a swappable instance; the Close icon carries a shape_full BOOLEAN_OPERATION — flatten it in the icon library.
Variants Inventory (8 total)

2 TitleAlignment × 2 FooterOrientation × hasSupportingText × hasDescription, built where the subtitle rule allows = 8 variants

TitleAlignmentFooterOrientationhasSupportingTexthasDescriptionNodeDimensions
LeftVerticalFalseTrue5304:32718360 × 404
LeftVerticalTrueFalse5377:35367360 × 398
LeftVerticalFalseFalse5377:35438360 × 372
CenterVerticalFalseTrue5304:32755360 × 378
LeftHorizontalFalseTrue5304:32769360 × 342
LeftHorizontalTrueFalse5377:35473360 × 336
LeftHorizontalFalseFalse5377:35510360 × 310
CenterHorizontalFalseTrue5304:32806360 × 316
1.0.2 — September 2026Patch
Style and Code tabs re-read · node 5304:32717
Style tab rebuilt from the eight live variants. Cards follow TitleAlignment — Left, Center — with FooterOrientation and the two subtitle booleans as panel controls; geometry, colours and the four text styles read off 5304:32718, 5304:32755, 5377:35367 and 5304:32769 against the exports; the preview is now class-based markup rather than inline styles. Padding, gap, alignment and colour tokens are not readable with the plugin and stay —.
Docs
Code tab regrouped on the four-property schema. Property Mapping one row per property with the five slots, Usage Snippets per TitleAlignment, Guidelines added, Scorecard regraded from the header-only era, Variants Inventory on the eight live nodes.
Docs
Close icon carries a BOOLEAN_OPERATION. The Trailing-Slot Close on 5304:32718 contains shape_full; flatten it in the icon library. Filed from the scorecard for the Component Review.
C6
1.0.1 — September 2026Patch
Documentation backfill · node 5304:32717
Resolved issues recorded. 19 resolved issues on the Overview tab had no changelog row: Scope corrected — the component is now the sheet, not its header; Content region is a real Figma Slot; Footer is a slot, not baked buttons; Header gained three named slots; Drag handle is a shared primitive; Component renamed to Bottom Sheet; footerOreintation typo corrected; Variant values moved to Title Case; Slots kebab-cased and frames moved to PascalCase; Header slots now carry real content; Title-Slot renamed Above-Title-Slot; Title text layer renamed Title; Drag Handle → DragHandle; Naming pass complete; Centred header confirmed control-free; Present, dismiss and detent contract documented; Boundary with Modal and Overlay recorded; Subtitle modelled as one enum; Centred sheets take the Description subtitle only — matrix complete at 8.
Docs
1.0.0 — April 2026Major
Initial Assessment · node 12817:43833
DS Health — 2 variants across 1 axis (alignment). Reusable and Composable both Fail: content is decorative placeholders, CTAs are hard-baked, no Slot architecture. Documented
Baseline
C1 — Component scope — Registered as "Bottom Drawer" but only models the sheet header + CTA area. Actual sheet primitives (drag handle, detents, scrim) absent. Open
C1
C1 — Content region — 4 decorative placeholder rectangles (UI Slot, SLOT 2..4) toggled by booleans instead of a Figma Slot. Open
C1
C2 — Alignment axis — Left vs Center are not just text-alignment; Center adds an above-title headerSlot and drops Close X. Two component shapes collapsed into one enum. Open
C2
C2 — Naming disagreement — Component named "Bottom Drawer", tokens in main/bottom-header/color/*, DS convention is "Bottom Sheet". Recommend rename to Bottom Sheet. Open
C2
C4 — Native mappability — No detent axis, no drag handle, hard-baked CTAs. Does not map to .sheet / ModalBottomSheet until restructure. Open
C4
C5 — Interaction states — No drag states, no empty / loading / error guidance for the content slot, no present / dismiss transition annotation. Open
C5
C6 — Raster close icon — Close X is a Figma CDN PNG (shape_full). Should be a vector Icon instance bound to main/bottom-header/color/icon-close. Open
C6
C7 — Code Connect — Blocked on restructure. Scope overlap with Modal (18507:71705) and Overlay (47:329691) must be resolved first. Open
C7
Family note — Recommended hierarchy: Overlay (scrim, shipped) → consumed by Modal (centered) + Bottom Sheet (bottom-anchored). Do not collapse Modal and Bottom Sheet; native APIs are distinct. Family
Family
Typography note — Description uses BarkAda (secondary font) at Secondary/Default/Base. Covered by the standing custom-font action item. Info
Info