Sheets
components specs/components/sheets.kmd
Surface anchored to an edge of the screen, slidable to reveal secondary content — bottom sheets (mobile primary) and side sheets (tablet/desktop). Material parity (`/components/bottom-sheets` and `/components/side-sheets`). Covers modal vs standard, drag gestures, scrim, focus trap, and snap points.
When this spec applies
Primary triggers
- Implement a bottom sheet
- Implement a side sheet
All triggers
- Add a peek-up panel from the bottom of mobile screen
- Show contextual options without leaving the page
- Reveal a secondary pane that user can dismiss
Specification body
Spec — Sheets
Facet Visual of Koder Design. Material parity: https://m3.material.io/components/bottom-sheets and https://m3.material.io/components/side-sheets.
2 anchor positions × 2 modalities
| Anchor | Modality | Mobile | Tablet/Desktop |
|---|---|---|---|
| Bottom — modal | Blocks page | ✓ Primary | Use dialog instead |
| Bottom — standard | Inline, page still interactive | ✓ Secondary | Rare |
| Side — modal | Blocks page | — | ✓ Primary |
| Side — standard | Inline, page still interactive | — | ✓ Common (3-pane layout) |
Pick anchor by surface: bottom on mobile (thumb-reach); side on tablet/desktop (wider real estate).
Anatomy (bottom sheet, modal)
▼ scrim (40% black overlay)
┌──────────────────────────────────────┐
│ ━━━ │ ← drag handle
│ Sheet title │
│ ────────────────────────────────── │
│ Content row 1 │
│ Content row 2 │
│ Content row 3 │
│ │
│ [Confirm] [Cancel] │
└──────────────────────────────────────┘
(anchored to bottom edge)
- Top corner radius: 28 px (bottom corners flush with screen)
- Drag handle: 4 px × 32 px pill, centered, 22 px from top
- Container bg:
surface-container-low - Elevation: 1 dp (modal scrim does the visual lift)
- Padding: 24 px horizontal, 16 px vertical
- Min height: 50% of viewport (default open state)
- Max height: 90% of viewport (leaves room to dismiss by tap above)
Anatomy (side sheet, standard)
┌────────────────────────┬───────────────────┐
│ │ Sheet title │
│ Main content │ ──────────────── │
│ │ Detail content │
│ │ │
│ │ │
└────────────────────────┴───────────────────┘
←── 320-400 dp ──→
- Width: 320-400 dp (fixed; not draggable in width)
- Anchor: right edge (default; left for RTL or 3-pane layouts)
- Border: 1 px
outline-varianton inner edge - Container bg:
surface-container-low - No corner radius on the screen-edge corners
R1 — Modality
| Modality | Scrim | Focus trap | Dismiss |
|---|---|---|---|
| Modal | Yes (40% black) | Yes | Scrim tap + drag-down + Esc + close × |
| Standard | No | No | Close × button only OR programmatic |
Modal sheet behaves like a dialog with bottom/side anchor: blocks page until dismissed. Standard sheet stays open and lets user interact with the rest of the page.
R2 — Bottom sheet snap points
| Snap | Height | Use |
|---|---|---|
| Closed | 0 px (hidden) | Initial state |
| Peek | 25% viewport OR ~120 px | Optional teaser visible |
| Half | 50% viewport | Default open |
| Expanded | 90% viewport | User dragged up |
| Full | 100% viewport | Becomes full-screen sheet |
Drag handle moves between snap points; velocity > 500 px/s expands or collapses past midpoint.
Peek snap is OPTIONAL — most sheets have only Closed → Half → Expanded.
R3 — Side sheet snap points
| Snap | Width | Use |
|---|---|---|
| Closed | 0 px (hidden) | Initial state |
| Open | 320-400 dp | Default |
| Wide | 50% viewport | User expanded (rare) |
Side sheets snap at fixed widths; don't free-resize like a window pane. If user needs free resize, use a resizable pane layout (not sheet).
R4 — Drag gesture
- Bottom sheet: drag handle bar OR anywhere in the sheet header
- Drag down: collapse to next snap (Expanded → Half → Closed)
- Drag up: expand to next snap (Half → Expanded)
- Velocity-based: fast flick passes through all snaps
- Side sheet: usually NO drag gesture (button-controlled)
- Content inside sheet scrolls independently — drag must originate on handle or header, not on scrolling content
Disabled when:
- Reduced motion preference active (still snaps, no smooth follow)
- Sheet is in Expanded state and content is mid-scroll
R5 — Scrim
Modal sheets ONLY. Scrim is 40% opacity black overlay covering the rest of the screen. Tap scrim → dismiss sheet.
Side sheet (modal) scrim covers the main content area, NOT the side sheet itself.
Standard sheets have NO scrim — user can interact with the page around the sheet.
R6 — Focus trap and keyboard
Modal sheets trap focus when open:
- Tab cycles within sheet, never escapes
- Esc dismisses (calls onDismiss callback)
- On open: focus moves to first focusable element OR sheet title (if labelled)
- On close: focus returns to the trigger element
Standard sheets do NOT trap focus — Tab moves naturally between sheet and page.
R7 — Mobile keyboard interaction
When mobile soft keyboard opens while a bottom sheet is showing:
- Sheet animates up to remain visible above keyboard
- Sheet snap point becomes "above keyboard" until keyboard closes
- Don't shrink sheet content height; let it scroll
R8 — Animation
- Open: slide-in from edge (motion-medium, ~250 ms) + scrim fade-in (modal only)
- Close: slide-out (motion-medium) + scrim fade-out
- Snap transition: spring animation (Material 3 emphasized decelerate, ~350 ms)
- Drag follow: 1:1 with finger; no spring during drag
- Reduced motion: instant in/out; no spring; no drag-follow easing (still works, just snaps)
R9 — Accessibility
- Modal sheet:
role="dialog"+aria-modal="true"+aria-labelledbypointing to sheet title - Standard sheet:
role="complementary"+aria-labeldescribing the sheet's purpose - Drag handle:
role="button"+aria-label="Drag to resize"+ keyboard support (Arrow Up/Down to expand/collapse) - Dismiss button:
aria-label="Close sheet" - Screen reader announces sheet on open: "Settings, dialog"
R10 — Per-preset variation
| Preset | Bottom sheet | Side sheet |
|---|---|---|
material3 | 28 px top corners, drag handle | Flush edges, no handle |
material2 | 16 px top corners, no handle | Flush, 4 dp shadow |
ios_cupertino | 16 px top corners, swipe-down to dismiss | Inspector-style overlay |
gnome | Adwaita BottomBar style, no handle (button-controlled) | Sidebar embedded in window |
windows_11 | Mica backdrop, system-style close × | Acrylic sidebar |
brutalist | Sharp top corners, 4 px thick top border | Sharp edges, thick border |
terminal_classic | ASCII box at bottom of screen | Vertical pane via tmux-style split |
R11 — Density
Inherits surface density from customization.kmd. Bottom sheet
default padding 24 px / 16 px → compact 16 px / 12 px → comfortable
32 px / 20 px.
R12 — Forbidden patterns
- ❌ Stacking sheets (sheet over sheet — use single sheet with navigation inside, or break into separate flows)
- ❌ Bottom sheet on Expanded/Large window-size class (use side sheet or dialog)
- ❌ Side sheet narrower than 320 dp (cramped)
- ❌ Side sheet wider than 50% of viewport (defeats sheet semantics; switch to full-page or dialog)
- ❌ Standard sheet with scrim (contradicts "non-modal")
- ❌ Modal sheet without focus trap
- ❌ Drag-to-resize that loses content position (scroll state must survive snap changes)
- ❌ Dismiss-by-scroll-content (content scroll triggers sheet dismissal — confusing; only handle drag dismisses)
- ❌ Bottom sheet without bottom safe-area inset
Cross-link
app-layout/safe-area.kmd— bottom sheet bottom insetapp-layout/window-size-classes.kmd— when to choose bottom vs sidethemes/elevation.kmd— modal scrim rolethemes/color-roles.kmd—surface-container-lowtokeninteraction/states.kmd— handle hover/presscomponents/dialogs.kmd— sibling modal pattern (centered vs edge-anchored)foundations/elements.kmd— Container family
References
specs/foundations/elements.kmdspecs/themes/elevation.kmdspecs/themes/color-roles.kmdspecs/interaction/states.kmdspecs/app-layout/safe-area.kmdspecs/components/dialogs.kmd