Build a Bottom Sheet Component for Mobile Web

“Build a bottom sheet” is a mobile-web frontend question that punishes shortcuts. Apple Maps, Apple Music, Slack iOS, Linear iOS all use bottom sheets as their primary modal pattern; on the web, the implementation is harder than it looks. This guide covers what interviewers probe for senior mobile-web roles.

Clarify scope

  • Single sheet height or multi-snap (collapsed, half, full)?
  • Drag-to-dismiss?
  • Backdrop dismiss?
  • Content longer than sheet — internal scroll?
  • iOS Safari vs Android Chrome — both?

The CSS foundation

  • Position fixed at bottom, off-screen by default
  • Translate Y based on snap state
  • Full-width on small screens; max-width on larger
  • Backdrop is a sibling element with opacity transitioning
.sheet {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  transform: translateY(var(--sheet-y));
  transition: transform 200ms ease-out;
  /* During drag, transition is none; CSS variable updates inline */
}

Snap points

  • Define snap points in pixels or percentages of viewport
  • e.g., [collapsed: 100px, half: 50%, full: 90%]
  • Drag releases snap to nearest based on velocity + position
  • State machine: current snap, target snap during drag

The drag gesture

  1. pointerdown on the handle (or anywhere if drag-anywhere): record start Y, start sheet Y
  2. pointermove: update sheet Y; throttle with rAF
  3. pointerup: compute velocity over last few moves; snap to closest snap point factoring velocity

Use pointer events with setPointerCapture for robustness across browsers.

The internal scroll wrinkle

The hard problem. The sheet has scrollable content. The user can:

  • Drag the handle to resize the sheet
  • Scroll the content within the sheet
  • If the content is at scrollTop = 0 and they drag down, should the sheet shrink or the page scroll?

The standard pattern: while scrollTop > 0, scroll the content. While scrollTop = 0 and the gesture is downward, drag the sheet. This requires careful gesture-handler coordination.

iOS Safari quirks

  • iOS Safari uses momentum scrolling (overscroll-behavior). Disable on the sheet container or it interferes with drag
  • The viewport bottom 88px is reserved for browser chrome on iOS — bottom sheets must respect this
  • iOS bounce can fight your drag; touch-action: pan-y; overscroll-behavior: contain on the sheet body
  • Safe-area-inset-bottom for the home indicator

The keyboard challenge

If the sheet has an input, the keyboard pushes content. Strategies:

  • Listen to the visualViewport API for keyboard show/hide
  • Resize the sheet to occupy what is left after the keyboard
  • Snap the sheet to “full” automatically when keyboard opens

VirtualViewport is supported on iOS Safari 13+ and Android Chrome.

Backdrop and dismissal

  • Backdrop fades from 0 to 0.4 opacity as sheet rises
  • Tap backdrop dismisses
  • Drag down past collapsed snap dismisses
  • Hardware back on Android dismisses (history.back integration)

Accessibility

  • Sheet is role="dialog" with aria-modal="true" when open
  • Focus trap inside the sheet
  • Focus returns to trigger on dismiss
  • Escape key dismisses on desktop
  • Drag handle has role="separator" with aria-valuenow if you support keyboard resize

Mobile-only or also desktop?

  • Some teams use bottom sheet only on mobile; modal on desktop
  • Others (Vercel, Linear) ship the same component on both
  • If responsive: max-width and centering on larger viewports turn it into a centered modal

Performance

  • Use transform, not top/bottom — GPU-accelerated
  • Throttle pointermove with rAF
  • Avoid re-renders during drag; manipulate the DOM directly via ref + CSS variables
  • Lazy-render content inside the sheet only when opened

Edge cases interviewers love

  • User flicks the sheet quickly — velocity-based snap
  • User drags past max snap — rubber-band resistance, do not exceed
  • Content at scrollTop = 0 + drag down should resize; at scrollTop > 0 should scroll
  • Keyboard opens mid-drag — handle the visualViewport event
  • Background audio playback context interrupts the gesture

What separates senior from staff

Senior implementations handle drag, snap, and dismiss. Staff implementations handle the internal-scroll vs sheet-drag coordination, the keyboard-aware sizing, and the safe-area inset on iOS. Principal-level discussion includes the responsive behavior across web and embedded webview contexts.

Frequently Asked Questions

Library options for production?

Vaul (by Emil Kowalski) is the modern React default. Mantine has one. Older: react-spring-bottom-sheet. Vaul handles most edge cases including the scroll-coordination problem.

How do I handle a bottom sheet inside a PWA standalone mode?

Standalone PWA loses the browser chrome but the iOS home indicator is still there. Respect env(safe-area-inset-bottom). Test on actual device in standalone mode; simulator misses the home indicator interactions.

What about portrait vs landscape?

Bottom sheets work in portrait; in landscape on phones, they cover too much. Switch to side sheet or modal in landscape.

Scroll to Top