Sheet
A bottom sheet slides up from the bottom of the screen. It's the primary mobile overlay pattern in Settld — used for expense details, category pickers, settings panels, and the settle-up confirmation flow. Three snap points: peek (40%), half (50%), full (90%). Dismisses by swiping down or tapping the backdrop.
Variants
Peek (40vh) — quick actions
₹4,800 · Paid by Priya · 4 people
Goa trip · 14 Dec
Half (50vh) — category picker
Full (90vh) — settle flow
Goa Airbnb (₹800) + Farzi Cafe (₹250) + fuel (₹150)
Snap points
| Snap | Class | Height | Use case |
|---|---|---|---|
| Peek | .sheet--peek | 40vh | Quick details, single confirm action — expense preview, delete confirmation |
| Half | .sheet--half | 50vh | Pickers, short lists — category, currency, payer |
| Full | .sheet--full | 90vh | Long forms, settle flow, settings — anything scrollable |
Snap points are initial heights. Drag gesture can slide between them. The full snap (90vh) leaves 10% of the screen showing behind — enough to see the backdrop blur and communicate "this is above the previous screen."
States
| State | Transform | Backdrop | Transition |
|---|---|---|---|
| Hidden | translateY(100%) | opacity 0, no pointer events | — |
| Opening | translateY(0) | opacity 1 | 420ms ease-ios |
| Open | translateY(0) | opacity 1 | — |
| Dragging | translateY(Npx) | opacity scales with drag | None (following pointer) |
| Closing | translateY(100%) | opacity 0 | 420ms ease-ios |
| Snap change | New height position | — | 240ms ease-ios |
On dark surfaces
₹4,800 · Paid by Priya
On dark surfaces, the sheet uses --dark-elevated background. The drag handle uses --dark-border. The footer separator uses the dark border. Secondary buttons use a translucent dark border.
Anatomy
| Part | Element | Notes |
|---|---|---|
| Backdrop | div.sheet-backdrop | Fixed inset:0, blur(2px), 48% black. Tap to close. |
| Container | div.sheet[role="dialog"] | Fixed, bottom:0, left:0, right:0. 28px top-radius. translateY(100%) when hidden. |
| Handle | div.sheet__handle | 32px wide, 4px tall, pill. Drag target — also entire sheet header is drag-able. |
| Header | div.sheet__header | Title + close button. Optional — omit for action-only sheets. |
| Body | div.sheet__body | Overflow-y scroll, overscroll-behavior contain. |
| Footer | div.sheet__footer | Sticky at bottom, top border. Primary action goes here. |
Usage
Accessibility
role="dialog" with aria-modal="true" and aria-labelledby pointing to the sheet title. The close button needs aria-label="Close". When the sheet opens, set aria-hidden="true" on the background content so screen readers don't read it.
Escape closes the sheet — this is non-negotiable. Tab cycles through interactive elements inside the sheet. The drag gesture is a touch-only interaction — ensure all sheet actions are also reachable by keyboard.
prefers-reduced-motion — when enabled, skip the translate animation and use opacity only (fade in/out).
Code
HTML
<!-- Backdrop --> <div class="sheet-backdrop" id="sheet-backdrop" aria-hidden="true"></div> <!-- Sheet --> <div class="sheet sheet--half" role="dialog" aria-modal="true" aria-labelledby="sheet-title" id="expense-sheet"> <div class="sheet__inner"> <div class="sheet__handle" aria-hidden="true"></div> <div class="sheet__header"> <h2 class="sheet__title" id="sheet-title">Goa Airbnb</h2> <button class="sheet__close" aria-label="Close">×</button> </div> <div class="sheet__body"> <!-- scrollable content --> </div> <div class="sheet__footer"> <button class="btn btn-teal btn-lg btn-block">Settle up →</button> </div> </div> </div>
JS — open / close
function openSheet(sheetId) { const sheet = document.getElementById(sheetId); const backdrop = document.getElementById('sheet-backdrop'); sheet.classList.add('open'); backdrop.classList.add('open'); document.body.style.overflow = 'hidden'; sheet.querySelector('.sheet__close')?.focus(); } function closeSheet(sheetId) { const sheet = document.getElementById(sheetId); const backdrop = document.getElementById('sheet-backdrop'); sheet.classList.remove('open'); backdrop.classList.remove('open'); document.body.style.overflow = ''; } // Close on backdrop click or Escape document.getElementById('sheet-backdrop') .addEventListener('click', () => closeSheet('expense-sheet')); document.addEventListener('keydown', e => { if (e.key === 'Escape') closeSheet('expense-sheet'); });
CSS classes
| Class | Purpose |
|---|---|
.sheet-backdrop | Blur + dim overlay, becomes visible on .open |
.sheet | Bottom sheet container — fixed, bottom:0 |
.sheet.open | translateY(0) — visible state |
.sheet--peek | 40vh height snap |
.sheet--half | 50vh height snap |
.sheet--full | 90vh height snap |
.sheet__inner | Full-height column flex layout |
.sheet__handle | Drag pill indicator, 32×4px |
.sheet__header | Title + close button row |
.sheet__title | Display font, 18px, 600 weight |
.sheet__close | 32px circular close button |
.sheet__body | Scrollable content area |
.sheet__footer | Sticky action area with top border |
Design tokens used
| Token | Value | Role |
|---|---|---|
--r-xl | 28px | Top corner radius |
--shadow-float | 0 30px 80px … | Sheet elevation |
--ease-ios | cubic-bezier(0.32,0.72,0,1) | Slide animation easing |
--d-slow | 420ms | Open/close duration |
--d-med | 240ms | Backdrop fade, snap duration |
--z-modal | 100 | Z-index layer |
--ink-line | #DDD2B8 | Handle color, footer border |