motif svelteArc

Bottom sheet ported

A sheet that rests at a peek or full height and follows your finger.

A modal sheet that rises from the bottom edge and rests at one or more detents, with drag, flick, and keyboard control. Built on Bits UI Dialog.

Live specimen

Loading demo…

Guidance

When to use

  • Mobile-first secondary tasks such as details, filters, or share options.
  • Content that benefits from a peek height before expanding to nearly full screen.
  • Maps, media, and detail views where users triage content between detents.

When not to use

  • Use dialog for interrupting decisions on any screen size.
  • Use drawer for side panels on wide desktop layouts.
  • Use popover for small anchored content that should not dim the page.

Installation

pnpm dlx shadcn-svelte@latest add @arcui/bottom-sheet

Usage

<script lang="ts">
  import { BottomSheet, BottomSheetClose } from '$lib/components/bottom-sheet';

  let open = $state(false);
</script>

<BottomSheet
  bind:open
  title="Trip Details — Lisbon"
  description="Oct 12 to Oct 15 · 3 nights"
  detents={[0.45, 0.9]}
>
  {#snippet trigger({ props })}
    <button {...props} type="button" class="btn-primary">View Trip</button>
  {/snippet}

  <div class="sheet-content">
    <p>Flight details, hotel reservations, and recommended activities.</p>
    <BottomSheetClose class="btn-secondary">Done</BottomSheetClose>
  </div>
</BottomSheet>

Variants & Examples

Dual Detent (Peek & Expand)

Starts at 45% peek height and expands to 92% on tap or drag.

<BottomSheet
  title="Details"
  detents={[0.45, 0.92]}
  initialDetent={0}
>
  <!-- Content -->
</BottomSheet>

Single Detent (Fixed Height)

Pass a single detent fraction for a fixed-height sheet that does not expand.

<BottomSheet
  title="Quick Share"
  detents={[0.5]}
>
  <!-- Content -->
</BottomSheet>

API Reference

PropTypeDefaultDescription
titlestring–Sheet title rendered as Dialog.Title in the header.
childrenSnippet–Scrollable sheet body content.
triggerSnippet<[{ props }]>–Control that opens the sheet; spread its props onto your button. Focus returns to it on close.
openbooleanundefinedControlled open state; supports two-way bind:open.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void–Called when the sheet opens or closes.
descriptionstring–Supporting line rendered as Dialog.Description below title.
detentsnumber[][0.45, 0.92]Resting heights as fractions of the viewport height.
initialDetentnumber0Index into sorted detents array the sheet opens at.
onDetentChange(index: number) => void–Called when the sheet settles on a different detent.
closeLabelstring'Close'Accessible label for the header close button.
classstring–Additional CSS class applied to the sheet surface.

Keyboard Interactions

KeyAction
ArrowUp / ArrowDownOn the grabber, moves one detent up or down.
Home / EndOn the grabber, jumps to tallest or smallest detent.
Enter / SpaceOn the grabber, toggles between smallest and tallest detent.
EscapeCloses the bottom sheet and returns focus to trigger.

Accessibility

  • Built on Bits UI Dialog: provides role="dialog", focus trap, and focus return to trigger.
  • Grabber is a semantic button with aria-expanded and localized Expand/Collapse label.
  • Detent changes are announced through a polite live status region.
  • Tabbing into content below the fold automatically expands sheet to reveal focused elements.

Motion

Phase 1 still-state: Sheet opens at detent height fraction, grabber button toggles peek/expanded with keyboard control. Phase 2 wires touch gesture velocity tracking, rubber band resistance past top detent, and dim opacity linked to sheet position.

Notes for AI

  • Use for mobile-first secondary tasks where a peek helps: details, filters, share options.
  • Pass a trigger snippet and spread its props onto your button, or control open yourself with bind:open.
  • Use a single-value detents array for a fixed-height sheet.

Source of truth: registry/components/bottom-sheet/bottom-sheet.tsx