Pular para o conteúdo

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.

Quando esta spec se aplica

Triggers primários

Todos os triggers

Corpo da especificação

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

AnchorModalityMobileTablet/Desktop
Bottom — modalBlocks page✓ PrimaryUse dialog instead
Bottom — standardInline, page still interactive✓ SecondaryRare
Side — modalBlocks page✓ Primary
Side — standardInline, 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-variant on inner edge
  • Container bg: surface-container-low
  • No corner radius on the screen-edge corners

R1 — Modality

ModalityScrimFocus trapDismiss
ModalYes (40% black)YesScrim tap + drag-down + Esc + close ×
StandardNoNoClose × 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

SnapHeightUse
Closed0 px (hidden)Initial state
Peek25% viewport OR ~120 pxOptional teaser visible
Half50% viewportDefault open
Expanded90% viewportUser dragged up
Full100% viewportBecomes 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

SnapWidthUse
Closed0 px (hidden)Initial state
Open320-400 dpDefault
Wide50% viewportUser 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-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 Up/Down to expand/collapse)
  • Dismiss button: aria-label="Close sheet"
  • Screen reader announces sheet on open: "Settings, dialog"

R10 — Per-preset variation

PresetBottom sheetSide sheet
material328 px top corners, drag handleFlush edges, no handle
material216 px top corners, no handleFlush, 4 dp shadow
ios_cupertino16 px top corners, swipe-down to dismissInspector-style overlay
gnomeAdwaita BottomBar style, no handle (button-controlled)Sidebar embedded in window
windows_11Mica backdrop, system-style close ×Acrylic sidebar
brutalistSharp top corners, 4 px thick top borderSharp edges, thick border
terminal_classicASCII box at bottom of screenVertical 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
  • 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

Requirements (testable)

Requirement: Modal vs standard modality {#req-sheets-modality}

A sheet SHALL apply modal treatment (40% black scrim, focus trap, and multi-path dismiss) when its modality is modal, and SHALL apply standard treatment (no scrim, no focus trap, close-button-or-programmatic dismiss only) when its modality is standard; a standard sheet SHALL NOT render a scrim.

Scenario: Modal sheet renders scrim and dismisses on scrim tap

  • GIVEN a bottom sheet configured with modality modal
  • WHEN the sheet is opened
  • THEN a scrim overlay at 40% opacity black covers the rest of the screen
  • AND a tap on the scrim invokes the sheet's dismiss and closes the sheet

Scenario: Standard sheet has no scrim and page stays interactive

  • GIVEN a side sheet configured with modality standard
  • WHEN the sheet is opened
  • THEN no scrim element is present in the rendered tree
  • AND a pointer event on the main content area outside the sheet reaches and activates the underlying page control

Requirement: Bottom sheet snap points and drag {#req-sheets-bottom-snap}

A bottom sheet SHALL move between its defined snap points (Closed 0px, Half ~50% viewport, Expanded ~90% viewport, plus optional Peek) in response to a drag originating on the drag handle or header, and a drag velocity greater than 500 px/s SHALL carry the sheet past the midpoint to the next snap in the drag direction.

Scenario: Drag up expands from Half to Expanded

  • GIVEN a bottom sheet resting at the Half snap point (~50% viewport height)
  • WHEN the user drags the drag handle upward past the midpoint toward Expanded
  • THEN the sheet settles at the Expanded snap point (~90% viewport height)

Scenario: Fast downward flick collapses to Closed

  • GIVEN a bottom sheet at the Half snap point
  • WHEN the user flicks the handle downward with velocity greater than 500 px/s
  • THEN the sheet passes through intermediate snaps and settles at Closed (dismissed)

Scenario: Scroll on content does not dismiss the sheet

  • GIVEN a bottom sheet whose content region is mid-scroll
  • WHEN a drag gesture originates on the scrolling content rather than the handle or header
  • THEN the sheet does not change snap point
  • AND the content scrolls independently

Requirement: Focus trap and keyboard on modal sheets {#req-sheets-focus-trap}

A modal sheet SHALL trap keyboard focus while open — Tab cycling within the sheet, Esc invoking the dismiss callback, focus moving on open to the first focusable element or the labelled title, and focus returning to the trigger element on close; a standard sheet SHALL NOT trap focus.

Scenario: Tab stays inside the modal sheet

  • GIVEN an open modal sheet containing two focusable controls
  • WHEN the user presses Tab repeatedly past the last focusable control
  • THEN focus cycles back to the first focusable control within the sheet and never lands on a page element behind the sheet

Scenario: Esc dismisses and restores trigger focus

  • GIVEN a modal sheet opened from a trigger button
  • WHEN the user presses Esc
  • THEN the sheet's onDismiss callback is invoked and the sheet closes
  • AND focus returns to the trigger button

Scenario: Standard sheet lets Tab leave the sheet

  • GIVEN an open standard sheet adjacent to interactive page content
  • WHEN the user presses Tab from the sheet's last focusable control
  • THEN focus moves to the next focusable element on the page outside the sheet

Requirement: Accessibility roles and labels {#req-sheets-a11y}

A sheet SHALL expose the correct ARIA semantics: a modal sheet with role="dialog", aria-modal="true", and aria-labelledby referencing its title; a standard sheet with role="complementary" and a descriptive aria-label; a drag handle with role="button", aria-label="Drag to resize", and Arrow Up/Down keyboard control; and a dismiss button with aria-label="Close sheet".

Scenario: Modal sheet advertises dialog semantics

  • GIVEN a modal bottom sheet whose title element has id sheet-title
  • WHEN the sheet is rendered open
  • THEN the sheet container has role="dialog", aria-modal="true", and aria-labelledby="sheet-title"

Scenario: Drag handle is keyboard operable

  • GIVEN a modal bottom sheet at the Half snap point with keyboard focus on the drag handle
  • WHEN the user presses Arrow Up
  • THEN the sheet expands to the next snap point
  • AND the handle exposes role="button" with aria-label="Drag to resize"

Requirement: Forbidden sheet configurations {#req-sheets-forbidden}

The sheet component SHALL NOT permit prohibited configurations: stacking a sheet over another sheet, rendering a bottom sheet on an Expanded/Large window-size class, a standard sheet with a scrim, a modal sheet without a focus trap, a side sheet narrower than 320 dp or wider than 50% of the viewport, or a bottom sheet without a bottom safe-area inset.

Scenario: Bottom sheet on large window class is rejected

  • GIVEN a large/expanded window-size class
  • WHEN a bottom sheet is requested
  • THEN the component refuses the bottom-sheet configuration and directs to a side sheet or dialog

Scenario: Bottom sheet includes bottom safe-area inset

  • GIVEN a bottom sheet rendered on a device reporting a nonzero bottom safe-area inset
  • WHEN the sheet is displayed at any open snap point
  • THEN the sheet content bottom padding includes the reported bottom safe-area inset

Scenario: Out-of-range side sheet width is rejected

  • GIVEN a side sheet requested with a width of 280 dp
  • WHEN the sheet is configured
  • THEN the configuration is rejected as below the 320 dp minimum

Referências