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.
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.
pnpm dlx shadcn-svelte@latest add @arcui/bottom-sheetUsage
<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
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | – | Sheet title rendered as Dialog.Title in the header. |
children | Snippet | – | Scrollable sheet body content. |
trigger | Snippet<[{ props }]> | – | Control that opens the sheet; spread its props onto your button. Focus returns to it on close. |
open | boolean | undefined | Controlled open state; supports two-way bind:open. |
defaultOpen | boolean | false | Initial open state when uncontrolled. |
onOpenChange | (open: boolean) => void | – | Called when the sheet opens or closes. |
description | string | – | Supporting line rendered as Dialog.Description below title. |
detents | number[] | [0.45, 0.92] | Resting heights as fractions of the viewport height. |
initialDetent | number | 0 | Index into sorted detents array the sheet opens at. |
onDetentChange | (index: number) => void | – | Called when the sheet settles on a different detent. |
closeLabel | string | 'Close' | Accessible label for the header close button. |
class | string | – | Additional CSS class applied to the sheet surface. |
Keyboard Interactions
| Key | Action |
|---|---|
ArrowUp / ArrowDown | On the grabber, moves one detent up or down. |
Home / End | On the grabber, jumps to tallest or smallest detent. |
Enter / Space | On the grabber, toggles between smallest and tallest detent. |
Escape | Closes 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