Sheets
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.
Spec — Sheets
Facet *isual*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 |
|---|---|---|---|
| *ottom — modal* | Blocks page | ✓ Primary | Use dialog instead |
| *ottom — standard* | Inline, page still interactive | ✓ Secondary | Rare |
| *ide — modal* | Blocks page | — | ✓ Primary |
| *ide — 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)- *op corner radius* 28 px (bottom corners flush with screen)
- *rag handle* 4 px × 32 px pill, centered, 22 px from top
- *ontainer bg*
surface-container-low - *levation* 1 dp (modal scrim does the visual lift)
- *adding* 24 px horizontal, 16 px vertical
- *in height* 50% of viewport (default open state)
- *ax height* 90% of viewport (leaves room to dismiss by tap above)
Anatomy (side sheet, standard)
┌────────────────────────┬───────────────────┐
│ │ Sheet title │
│ Main content │ ──────────────── │
│ │ Detail content │
│ │ │
│ │ │
└────────────────────────┴───────────────────┘
←── 320-400 dp ──→- *idth* 320-400 dp (fixed; not draggable in width)
- *nchor* right edge (default; left for RTL or 3-pane layouts)
- *order* 1 px
outline-varianton inner edge - *ontainer bg*
surface-container-low - *o corner radius*on the screen-edge corners
R1 — Modality
| Modality | Scrim | Focus trap | Dismiss |
|---|---|---|---|
| *odal* | Yes (40% black) | Yes | Scrim tap + drag-down + Esc + close × |
| *tandard* | 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 |
|---|---|---|
| *losed* | 0 px (hidden) | Initial state |
| *eek* | 25% viewport OR ~120 px | Optional teaser visible |
| *alf* | 50% viewport | Default open |
| *xpanded* | 90% viewport | User dragged up |
| *ull* | 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 |
|---|---|---|
| *losed* | 0 px (hidden) | Initial state |
| *pen* | 320-400 dp | Default |
| *ide* | 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
- *ottom 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
- *ide 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
- *pen* slide
in from edge (motionmedium, ~250 ms) +scrim fade-in (modal only)
- *lose* slide
out (motionmedium) + scrim fade-out - *nap transition* spring animation (Material 3 emphasized
decelerate, ~350 ms)
- *rag 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-labeldescribingthe sheet's purpose
- Drag handle:
role="button"+aria-label="Drag to resize"+keyboard support (Arrow UpDown to expandcollapse)
- 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
toresize that loses content position (scroll state mustsurvive snap changes)
- ❌ Dismiss
byscroll-content (content scroll triggers sheetdismissal — 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