Sheets

mandatory

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-variant on 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* slidein from edge (motionmedium, ~250 ms) +

    scrim fade-in (modal only)

  • *lose* slideout (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-labelledby pointing to sheet title

  • Standard sheet: role="complementary" + aria-label describing

    the 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
  • ❌ Dragtoresize that loses content position (scroll state must

    survive snap changes)

  • ❌ Dismissbyscroll-content (content scroll triggers sheet

    dismissal — confusing; only handle drag dismisses)

  • ❌ Bottom sheet without bottom safe-area inset
  • app-layout/safe-area.kmd — bottom sheet bottom inset
  • app-layout/window-size-classes.kmd — when to choose bottom vs side
  • themes/elevation.kmd — modal scrim role
  • themes/color-roles.kmdsurface-container-low token
  • interaction/states.kmd — handle hover/press
  • components/dialogs.kmd — sibling modal pattern (centered vs edge-anchored)
  • foundations/elements.kmd — Container family

Source: ../home/koder/dev/koder/meta/docs/stack/specs/components/sheets.kmd