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

Goa Airbnb

₹4,800 · Paid by Priya · 4 people

Goa trip · 14 Dec

Half (50vh) — category picker

Choose category
🍜 Food & drink
✈︎ Travel
⌂ Stay
🎭 Entertain
🛒 Shopping
Other

Full (90vh) — settle flow

Settle up
₹1,200
You owe Priya

Goa Airbnb (₹800) + Farzi Cafe (₹250) + fuel (₹150)

📱
GPay
Google Pay UPI
📲
PhonePe
PhonePe UPI

Snap points

SnapClassHeightUse case
Peek.sheet--peek40vhQuick details, single confirm action — expense preview, delete confirmation
Half.sheet--half50vhPickers, short lists — category, currency, payer
Full.sheet--full90vhLong 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

Hidden
Peek
Open
StateTransformBackdropTransition
HiddentranslateY(100%)opacity 0, no pointer events
OpeningtranslateY(0)opacity 1420ms ease-ios
OpentranslateY(0)opacity 1
DraggingtranslateY(Npx)opacity scales with dragNone (following pointer)
ClosingtranslateY(100%)opacity 0420ms ease-ios
Snap changeNew height position240ms ease-ios

On dark surfaces

Goa Airbnb

₹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

Title
×
Sheet body content…
Footer actions
Handle (32×4px)
Close button
PartElementNotes
Backdropdiv.sheet-backdropFixed inset:0, blur(2px), 48% black. Tap to close.
Containerdiv.sheet[role="dialog"]Fixed, bottom:0, left:0, right:0. 28px top-radius. translateY(100%) when hidden.
Handlediv.sheet__handle32px wide, 4px tall, pill. Drag target — also entire sheet header is drag-able.
Headerdiv.sheet__headerTitle + close button. Optional — omit for action-only sheets.
Bodydiv.sheet__bodyOverflow-y scroll, overscroll-behavior contain.
Footerdiv.sheet__footerSticky at bottom, top border. Primary action goes here.

Usage

Do Use sheets for contextual overlays that relate to a list item or selected element. The sheet appears over the list — the user can see what they were looking at. Always include a drag handle and a way to close (drag down, tap backdrop, or × button).
Don't Don't use a sheet for global settings or navigation flows that don't relate to a specific item — use a full-page route instead. Don't stack sheets on top of each other more than one level deep.
Do Use the peek (40vh) snap for simple confirm/cancel actions. Use half (50vh) for pickers. Use full (90vh) for the settle flow, which needs space for method selection and a clear CTA.
Don't Don't put form inputs that will trigger the keyboard at the bottom of a full sheet — the keyboard will push them off screen. Scroll the body up, or use a sticky header pattern with the input at the top.

Accessibility

Focus trap When the sheet opens, trap focus inside — keyboard users should not be able to Tab to content behind the backdrop. Return focus to the trigger element that opened the sheet when it closes.
ARIA Use 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.
Keyboard 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.
Motion The slide-in animation runs at 420ms. Respect 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

ClassPurpose
.sheet-backdropBlur + dim overlay, becomes visible on .open
.sheetBottom sheet container — fixed, bottom:0
.sheet.opentranslateY(0) — visible state
.sheet--peek40vh height snap
.sheet--half50vh height snap
.sheet--full90vh height snap
.sheet__innerFull-height column flex layout
.sheet__handleDrag pill indicator, 32×4px
.sheet__headerTitle + close button row
.sheet__titleDisplay font, 18px, 600 weight
.sheet__close32px circular close button
.sheet__bodyScrollable content area
.sheet__footerSticky action area with top border

Design tokens used

TokenValueRole
--r-xl28pxTop corner radius
--shadow-float0 30px 80px …Sheet elevation
--ease-ioscubic-bezier(0.32,0.72,0,1)Slide animation easing
--d-slow420msOpen/close duration
--d-med240msBackdrop fade, snap duration
--z-modal100Z-index layer
--ink-line#DDD2B8Handle color, footer border