The bottom-anchored sheet surface — a drag handle, a slotted header, an optional subtitle, and content and footer slots a consumer fills.
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.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.
Preamble here...
Title here of the header...
This is a body description
content slot removed the reason every product usage previously detached or spawned a local variant.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.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.| 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. |
- Scope corrected — the component is now the sheet, not its header. v2.0: Rebuilt on node
5304:32717in the 2026 Working File. The shell isdragHandle→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.
contentis a genuineSLOTnode, 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:
footeris aSLOTcarrying twoButton - Large/Mediuminstances as default content rather than as fixed structure. Action count and pairing are now the consumer’s decision, and thefooterOreintationaxis 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,leadingSlotandtrailingSlotare all realSLOTnodes around atitleBlock. 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:
dragHandleis an instance wrapping a 32×4#C2CFE5pill 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
-
footerOreintationtypo corrected. v2.1: Verified on the live node — all eight variants now readfooterOrientation. 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 | HorizontalandTrue | Falseacross 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 framesheaderanddescriptionare nowHeaderandDescription, and#description→Description, dropping the last legacy sigil. (C1 · Rename) C1 · Layer Structure & Naming - Header slots now carry real content. v2.1:
Leading-Slotholds aPlaceholderinstance as its swap target andTrailing-Slotholds aCloseicon instance built on ashape_fullboolean 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-SlotrenamedAbove-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:35197was calledHeaderinside a frame also calledHeader— two things under one name in a single variant. It now readsTitle, which resolves the collision and completes the §7 hierarchy alongside its siblingPreamble. (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.titleRowandtitleBlockare nowTitleRowandTitleBlock, and the enum properties readTitleAlignmentandFooterOrientationin 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=CenterdropsLeading-SlotandTrailing-Slotbecause 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 usesTitleAlignment=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-Slotand 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 — theDragHandleis 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.sheetwith.presentationDetents([.height(contentHeight), .large]),.presentationDragIndicator(.visible)and.interactiveDismissDisabled()where dismissal is blocked; AndroidModalBottomSheetwithsheetState,dragHandle = { BottomSheetDefaults.DragHandle() }andproperties = 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×hasDescriptionis replaced bySubtitle = 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:Centerships only theDescriptionsubtitle, 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.
Centeris the layout for confirmations, successes and celebrations, where the surface itself is the message — so it takes the fullerDescriptiontreatment, whileSupportingis the denser, left-aligned option andNonebelongs to sheets whose content carries the meaning. The fourCenter×Supporting|Nonecombinations are therefore unsupported rather than unbuilt, and eight is the complete matrix:Left× 3 subtitles × 2 footer orientations = 6, plusCenter×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
Preamble here...
Title here of the header...
This is a body description
| 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 |
Preamble here...
Title here of the header...
This is description
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.*
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 Property | SwiftUI | Compose |
|---|---|---|
| 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 = "…" |
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 = { … }
) 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 = { … }
) | Requirement | iOS | Android |
|---|---|---|
| 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. |
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.
| 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. |
2 TitleAlignment × 2 FooterOrientation × hasSupportingText × hasDescription, built where the subtitle rule allows = 8 variants
| TitleAlignment | FooterOrientation | hasSupportingText | hasDescription | Node | Dimensions |
|---|---|---|---|---|---|
| Left | Vertical | False | True | 5304:32718 | 360 × 404 |
| Left | Vertical | True | False | 5377:35367 | 360 × 398 |
| Left | Vertical | False | False | 5377:35438 | 360 × 372 |
| Center | Vertical | False | True | 5304:32755 | 360 × 378 |
| Left | Horizontal | False | True | 5304:32769 | 360 × 342 |
| Left | Horizontal | True | False | 5377:35473 | 360 × 336 |
| Left | Horizontal | False | False | 5377:35510 | 360 × 310 |
| Center | Horizontal | False | True | 5304:32806 | 360 × 316 |
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 —.Close on 5304:32718 contains shape_full; flatten it in the icon library. Filed from the scorecard for the Component Review.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.alignment). Reusable and Composable both Fail: content is decorative placeholders, CTAs are hard-baked, no Slot architecture. DocumentedUI Slot, SLOT 2..4) toggled by booleans instead of a Figma Slot. Openmain/bottom-header/color/*, DS convention is "Bottom Sheet". Recommend rename to Bottom Sheet. Open.sheet / ModalBottomSheet until restructure. Openshape_full). Should be a vector Icon instance bound to main/bottom-header/color/icon-close. Open18507:71705) and Overlay (47:329691) must be resolved first. OpenBarkAda (secondary font) at Secondary/Default/Base. Covered by the standing custom-font action item. Info