The bottom-anchored sheet surface used for list pickers, confirmations, forms, and onboarding.
Modal (18507:71705) and Overlay (47:329691), all three independently declaring what a floating-surface-over-scrim looks like. Proposal: rebuild as EBBottomSheet — a thin wrapper around SwiftUI .sheet / Compose ModalBottomSheet — with explicit dragHandle, header, content, and footer slots, and consume the already-assessed Overlay for the scrim.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.
main/bottom-header/color/*, but the component is named Bottom Drawer — neither name matches common usage "Bottom Sheet". The Alignment axis silently changes the shape of the component (Center drops Close X, adds a headerSlot). Two axis values behave like two separate components.| 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. |
- Component scope is the header, not the sheet. "Bottom Drawer" only models the rounded top surface plus a header and hard-baked CTA row. The actual sheet primitives — drag handle, detents, scrim, snap behaviour, swipe-down-to-dismiss — are absent. Every product usage in the context file (
12522:109042) has to re-compose the sheet by hand. C1 · Layer Structure & Naming - Content region is 4 decorative placeholder rectangles, not a Slot. Inside the body,
UI Slot,SLOT 2,SLOT 3,SLOT 4are pink-dashed#FFECF8rectangles toggled by booleansshowSlot1..4. They are not Figma Slots — designers can't instance-swap in Action List rows, form fields, or Filter chips without detaching. C1 · Layer Structure & Naming - Alignment axis hides a second component. Left Align and Center Align are not just text-alignment differences: Left Align has an icon placeholder + title block + Close X on the right; Center Align has a separate
headerSlot(used for progress bars / steppers) + title block and no Close X. These are two distinct layouts collapsed into one enum. C2 · Variant & Property Naming - No drag handle primitive. Nothing in the Figma tree renders a drag handle ("grabber"). iOS and Android expect this as an explicit visual affordance the user grabs to resize. Either add a handle node bound to a token, or document that rendering is delegated to the platform primitive. C5 · Interaction State Coverage
- No detent axis. There's no
medium / large / fitContentaxis on the component. Sheet height is whatever the decorative slots sum to. Native APIs require a discrete detent set; the Figma model doesn't reflect this. C4 · Native Mappability - Scope overlap with Modal and Overlay. Bottom Sheet, Modal (
18507:71705), and Overlay (47:329691) all independently model "surface above a scrim". None of them compose each other. The scrim should live in Overlay (already assessed); Modal and Bottom Sheet should consume it. C7 · Code Connect Linkability - Close X is raster and asymmetric. The close icon is a Figma CDN PNG asset (
shape_full) rendered only on Left Align — Center Align has no dismiss affordance at all. Both alignments should offer the same affordance, and it should be a vector Icon instance bound tomain/bottom-header/color/icon-close. C6 · Asset & Icon Quality - CTAs are hard-baked, not composable. A single primary button + a single tertiary button are instance-swapped inside the component with booleans
primaryAction/secondaryAction. Consumers who need one button, two horizontal buttons, three stacked options, or a link-only footer have to detach. CTA should be a footer slot receiving any button composition. C4 · Native Mappability - Icon placeholder is a raw grey circle, not a Slot. Left Align's leading icon is a hardcoded
#C2C6CFcircle inside anicon-placeholderframe. Same anti-pattern as Modal's icon slot. Should be a Figma Slot backed by the Icon component. C1 · Layer Structure & Naming - Token namespace and component name disagree. The component is named "Bottom Drawer" while its tokens live in
main/bottom-header/color/*. DS literature (Material, HIG) and this report use "Bottom Sheet". Pick one name and propagate: rename the component, rename the token collection, or both. C2 · Variant & Property Naming - No Code Connect mapping. Blocked until the restructure lands — mapping the current schema would hardcode the wrong architecture. C7 · Code Connect Linkability
- Restructure around a clean shell with four named slots. Target shape:
EBBottomSheet(isPresented, detents, dragHandle, header, content, footer). header = optional title bar region (title, preamble, leading icon, trailing icon / close). content = the one and only body Slot — accepts any DS composition (Action List, form fields, filter chips, tips list). footer = button group pinned to the bottom. Every current product usage becomes a composition: list picker = BottomSheet + Action List; confirmation = BottomSheet + description + button group; form = BottomSheet + Labeled Fields + button group; tips list = BottomSheet + numbered list; welcome card = BottomSheet + illustration + button group. Property - Promote the body to a real Figma Slot. Replace the 4 pink-dashed placeholder rectangles with a single named
contentSlot (Figma's Slot feature). Default to an empty 24-padded frame; let consumers instance-swap in Action List rows, form fields, or any other DS primitive without detaching. Slot - Consolidate the Bottom Sheet / Modal / Overlay family. Canonical hierarchy:
Overlay(scrim primitive, already shipped) → consumed by bothModal(centered dialog) andBottom Sheet(bottom-anchored sheet). The three ship distinct anchor positions but share the scrim. Do not collapse Modal and Bottom Sheet into one — native platforms treat them as separate APIs (.sheetvs.alert/DialogvsModalBottomSheet). Family - Replace the Alignment enum with a proper header schema. Split the silent shape-shift into explicit properties:
titleAlignment = left | center(just text-align),leadingSlot(icon / avatar / empty),trailingSlot(close X / icon button / empty),aboveTitleSlot(stepper / progress bar / empty). Both alignments now share the same structural shape, just different text-align and slot content. Property - Add an explicit detent axis. Introduce
Detent = medium | large | fitContentas a Figma variant — even if visually similar, this makes the Code Connect mapping 1:1 with.presentationDetents([.medium, .large])/SheetValue.PartiallyExpanded. Designers can then show in mocks which detent a sheet resolves to. Property - Add a drag-handle primitive. Vector rect, 32×4, radius 4, bound to a new token
main/bottom-header/color/drag-handle(suggest#C2C6CF). Ship as its own tiny component so Modal-style sheets can omit it and Bottom Sheet can include it. Default visible for Bottom Sheet. Asset - Footer action group should be a slot, not baked buttons. Replace the
primaryAction+secondaryActionbooleans with afooterslot that accepts any button composition — 0, 1, 2 horizontal, 2 vertical, link-only, icon+label. Same fix that Modal needs, and both should share a newEBButtonGroupprimitive if the team wants to keep the DS tight. Slot - Replace the raster close icon with a vector instance. Swap the Figma CDN PNG close for the DS vector Close icon, and bind colour to
main/bottom-header/color/icon-close(#6780A9). Available on both alignments via the trailing slot. Asset - Convert the leading icon-placeholder into an Icon slot. Same pattern as Modal: add a Figma Slot for the leading icon backed by the Icon component so designers can swap without detaching. Default to a neutral status icon or nothing. Slot
- Rename the component and its token namespace. Pick one: either rename the component to Bottom Sheet and rename the token collection from
main/bottom-header/color/*tomain/bottom-sheet/color/*, or keep "Drawer" and align tokens tomain/bottom-drawer/*. Current disagreement between component name, token name, and common DS vocabulary costs designers every time they search. Recommended: rename to Bottom Sheet to match Material / HIG / this assessment. Rename - Annotate the present / dismiss / gesture contract. Document on the component: slide-up entrance, fade-out-with-scrim exit, swipe-down-to-dismiss, tap-outside-dismiss, ESC/back button behaviour, focus trap, restore-focus-on-close. Developers currently have to infer these from adjacent patterns. Docs
- Add a dismissible/modal switch. Some flows (transfer confirmation, destructive action) need a non-swipe-dismiss sheet. Surface this as
dismissible: boolon the component, mapping to iOS.interactiveDismissDisabled(!dismissible)and ComposesheetState.confirmValueChange. State
A modal sheet that slides up from the bottom of the screen — typically used to confirm an action, collect a single input, or surface a focused decision without leaving the current screen.
Bottom sheet with preamble, heading, description, optional close icon, and primary CTA.
| Role | Token | Default |
|---|---|---|
| Surface | bottom-header/color/bg | #FFFFFF |
| Preamble | bottom-header/color/preamble | #90A8D0 |
| Header | bottom-header/color/header | #0A2757 |
| Description | bottom-header/color/description | #445C85 |
| Close icon | bottom-header/color/icon-close | #6780A9 |
The current Figma schema (alignment + 9 booleans) is not fit for 1:1 mapping. The table below maps the proposed post-restructure schema to native APIs.
| Figma Property | SwiftUI | Compose |
|---|---|---|
isPresented | .sheet(isPresented: $binding) | if (showSheet) ModalBottomSheet(onDismissRequest:) |
detents | .presentationDetents([.medium, .large]) | sheetState = rememberModalBottomSheetState(…) + Detent enum |
dragHandle = visible|hidden | .presentationDragIndicator(.visible / .hidden) | dragHandle = { BottomSheetDefaults.DragHandle() } or null |
titleAlignment = leading|center | titleAlignment: .leading / .center | titleAlignment = Alignment.Start / Center |
leading slot | leading: { EBAvatar(…) } (ViewBuilder) | leading: @Composable () -> Unit |
trailing slot (e.g. close) | trailing: { EBIconButton(.close) { dismiss() } } | trailing: @Composable () -> Unit |
aboveTitle slot (progress / stepper) | aboveTitle: { EBProgressBar(…) } | aboveTitle: @Composable () -> Unit |
preamble | preamble: String? | preamble: String? = null |
title | title: String | title: String |
description | description: String? | description: String? = null |
content slot | @ViewBuilder content: () -> Content (trailing closure) | content: @Composable ColumnScope.() -> Unit |
footer slot | footer: () -> Footer | footer: @Composable RowScope.() -> Unit |
dismissible | .interactiveDismissDisabled(!dismissible) | sheetState.confirmValueChange = { dismissible } |
(legacy) alignment = Left Align | → split into titleAlignment + leading + trailing | → split into titleAlignment + leading + trailing |
(legacy) showSlot1..4 booleans | → removed, replaced by content slot | → removed, replaced by content slot |
(legacy) primaryAction / secondaryAction | → removed, replaced by footer slot | → removed, replaced by footer slot |
| Requirement | iOS | Android |
|---|---|---|
| Modal trait | .sheet applies the modal trait automatically — VoiceOver traps focus inside the sheet. | ModalBottomSheet treats content as modal by default — TalkBack swipe is contained. |
| Focus management | Focus moves to the sheet on present; restores to trigger on dismiss. If a first-field focus is desired, use .focused($firstField). | Focus enters sheet content on show; restored to trigger on dismiss. Request initial focus via LaunchedEffect + focusRequester. |
| Title as heading | Mark the title Text with .accessibilityAddTraits(.isHeader) so VoiceOver reads it first. | Use Modifier.semantics { heading() } on the title; set paneTitle on the sheet surface. |
| Drag handle announcement | iOS's built-in grabber is announced as "Adjustable". Custom handles need .accessibilityLabel("Resize sheet") and .accessibilityAdjustableAction. | Material 3 default handle exposes resize action. Custom handles need Modifier.semantics { contentDescription = "Resize sheet" }. |
| Dismiss gesture | Swipe-down + ESC + tap-outside all route through isPresented. For non-dismissible, use .interactiveDismissDisabled(true). | Back gesture + tap-outside via onDismissRequest. Non-dismissible: sheetState.confirmValueChange = { false }. |
| Close button (if trailing slot) | Wrap 24×24 icon in a ≥44×44pt tappable area. Label: .accessibilityLabel("Close"). | Wrap 24×24 icon in a ≥48×48dp tappable area. contentDescription = stringResource(R.string.close). |
| Destructive CTA | Use role: .destructive on the footer button. | Use EBButtonDefaults.destructiveColors() and explicit contentDescription. |
| Reduce motion | Respect UIAccessibility.isReduceMotionEnabled — skip slide-up / use cross-fade. | Respect Settings.Global.ANIMATOR_DURATION_SCALE — shorten animation when accessibility demands it. |
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Requires Rework | Scope is the header, not the sheet. Content region is 4 decorative placeholder rectangles instead of a Figma Slot. Icon-placeholder is a raw circle. Component name ("Bottom Drawer") disagrees with token namespace ("bottom-header") and DS convention ("Bottom Sheet"). |
| C2 | Variant & Property Naming | Requires Rework | Alignment axis collapses two structurally different layouts (Left has leading icon + close X; Center has above-title headerSlot + no close X). No detent axis. 9 booleans (showSlot1..4, primaryAction, secondaryAction, preamble, description, iconPlaceholder) that should be slots. |
| C3 | Token Coverage | Needs Refinement | Surface, preamble, header, description, close icon all bound to main/bottom-header/color/*. Icon-placeholder grey (#C2C6CF) is hardcoded. Drag-handle token doesn't exist yet (component has no handle). |
| C4 | Native Mappability | Requires Rework | Does not map to .sheet / ModalBottomSheet as-is. No detent axis, no drag handle, no dismissible contract, hard-baked CTAs. After restructure → clean 1:1 mapping. |
| C5 | Interaction State Coverage | Requires Rework | Only default state. No drag states (resting / dragging / snapping), no present / dismiss transition annotation, no empty / loading / error state guidance for the content slot. |
| C6 | Asset & Icon Quality | Needs Refinement | Close X is a remote raster PNG (shape_full from Figma CDN) rather than a vector Icon instance. Icon-placeholder is a raw #C2C6CF circle, not a vector icon slot. |
| C7 | Code Connect Linkability | Not Mapped | Blocked on restructure. Scope overlap with Modal and Overlay must be resolved; mapping the current schema would hardcode the wrong architecture. |
Single axis — Alignment = Left Align | Center Align. The header shape-shifts across these two values (see Open Issues).
| # | Alignment | Node | Dimensions | Header slots present | Notes |
|---|---|---|---|---|---|
| 1 | Left Align | 12522:12860 | 360 × 324 | iconPlaceholder (leading) · preamble · title · Close X (trailing, raster) | Title + preamble left-aligned next to optional leading icon. Close X fixed top-right at (24, 24). |
| 2 | Center Align | 12817:43834 | 360 × 330 | headerSlot (above-title, e.g. progress bar) · preamble · title | No leading icon, no close X. Adds an headerSlot used for progress bars / steppers. Title + preamble centered. |
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