Sora UI

Bottom Sheet

A draggable, snap-point bottom sheet built on Radix Dialog for accessibility, with Motion driving the drag gesture and glide.

Made by Axyl

A mobile-style settings panel: drag to dismiss, snap points, and a customizable row list.

Installation

File Structure

bottom-sheet.tsx
use-controlled-state.tsx
use-prefers-reduced-motion.tsx
get-strict-context.tsx
ease.tsx

Usage

import {
  BottomSheet,
  BottomSheetContent,
  BottomSheetDescription,
  BottomSheetList,
  BottomSheetPanel,
  BottomSheetRow,
  BottomSheetTitle,
  BottomSheetTrigger,
} from '@/components/sora-ui/radix/bottom-sheet';

export default function Page() {
  return (
    <BottomSheet>
      <BottomSheetTrigger>Open settings</BottomSheetTrigger>
      <BottomSheetContent>
        <BottomSheetTitle>Settings</BottomSheetTitle>
        <BottomSheetDescription>Adjust sound and theme.</BottomSheetDescription>
        <BottomSheetPanel>
          <BottomSheetList>
            <BottomSheetRow label="Sound" value="On" />
            <BottomSheetRow label="Theme" value="Dark" />
          </BottomSheetList>
        </BottomSheetPanel>
      </BottomSheetContent>
    </BottomSheet>
  );
}

For a fully custom row (not the label — value layout), pass children to BottomSheetRow instead of label/value, or skip BottomSheetPanel/BottomSheetList/BottomSheetRow entirely and render your own content inside BottomSheetContent.

Props

BottomSheet

PropTypeDefault
open?
boolean
-
defaultOpen?
boolean
false
onOpenChange?
(open: boolean) => void
-

BottomSheetContent

PropTypeDefault
snapPoints?
(number | "auto")[]
[0.5, 0.92]
defaultSnap?
number
0
dismissThreshold?
number
120
showHandle?
boolean
true
handleClassName?
string
-
overlay?
boolean
true
overlayClassName?
string
-

BottomSheetRow

PropTypeDefault
label?
React.ReactNode
-
value?
React.ReactNode
-
children?
React.ReactNode
-
labelClassName?
string
-
valueClassName?
string
-
lineClassName?
string
-

When to Use

Use the Bottom Sheet for mobile-first actions that need more room than a dropdown but shouldn't take over the whole screen — settings panels, filters, or action lists reachable with a thumb. snapPoints lets it act as a half-height quick-glance panel that expands to near-fullscreen on a second drag.

Avoid it for content that must stay reachable on desktop without a pointer-drag affordance, or for anything that must block navigation entirely — use a centered dialog instead.

Accessibility

Built on Radix Dialog, so focus is trapped inside the sheet while open, Esc and an outside click close it, and focus returns to the trigger on close. Radix requires both BottomSheetTitle and BottomSheetDescription on every sheet — both default to sr-only, so include them even when the sheet doesn't need a visible heading (Radix logs a console warning if BottomSheetDescription is missing). When prefers-reduced-motion: reduce is set, the sheet fades in/out instead of sliding.

Credits

Inspired by Vaul.

Built by Axyl. A motion-first component registry for React.

Last updated: 7/4/2026