Bottom Sheet
beui/bottom-sheetFreeComponent
Vaul-inspired draggable bottom sheet with snap points, inertia and glass surface.
Install it
npx shadcn@latest add https://beui.dev/r/bottom-sheet.jsonSource
1"use client";2// beui.dev/components/motion/bottom-sheet34import {5 AnimatePresence,6 motion,7 type PanInfo,8 useDragControls,9 useReducedMotion,10} from "motion/react";11import { type ReactNode, useEffect, useRef, useState } from "react";12import { createPortal } from "react-dom";13import { EASE_DRAWER } from "@/lib/ease";14import { cn } from "@/lib/utils";1516// Vaul-style glide: a long, fully-damped tween reads smoother than a spring on17// open — no settle/overshoot, just one clean decel. Same curve drives the18// backdrop fade so the surface and scrim move as one.19const DRAWER = { duration: 0.5, ease: EASE_DRAWER } as const;2021export interface BottomSheetProps {22 open: boolean;23 onOpenChange: (open: boolean) => void;24 /** Heights (0-1 = fraction of viewport, or "auto"). First entry is default. */25 snapPoints?: (number | "auto")[];26 defaultSnap?: number;27 title?: string;28 description?: string;29 children?: ReactNode;30 className?: string;31 /** Min drag distance (px) past current snap to dismiss. */32 dismissThreshold?: number;33}3435export function BottomSheet({36 open,37 onOpenChange,38 snapPoints = [0.5, 0.92],39 defaultSnap = 0,40 title,41 description,42 children,43 className,44 dismissThreshold = 120,45}: BottomSheetProps) {46 const [snap, setSnap] = useState(defaultSnap);47 const [mounted, setMounted] = useState(false);48 const dragControls = useDragControls();49 const sheetRef = useRef<HTMLDivElement>(null);50 const reduce = useReducedMotion();51 const heightRef = useRef(0);5253 useEffect(() => {54 setMounted(true);55 }, []);5657 useEffect(() => {58 if (open) setSnap(defaultSnap);59 }, [open, defaultSnap]);6061 // Lock background scroll while open. overflow:hidden alone is ignored by62 // iOS Safari — boundary scrolls inside the sheet chain to the page, which63 // scrolls underneath and ends up somewhere else on close. position:fixed64 // is the lock that actually holds; restore the scroll position after.65 useEffect(() => {66 if (!open) return;67 const body = document.body;68 const scrollY = window.scrollY;69 const prev = {70 position: body.style.position,71 top: body.style.top,72 left: body.style.left,73 right: body.style.right,74 overflow: body.style.overflow,75 };76 body.style.position = "fixed";77 body.style.top = `-${scrollY}px`;78 body.style.left = "0";79 body.style.right = "0";80 body.style.overflow = "hidden";81 return () => {82 body.style.position = prev.position;83// … truncated
What it pulls in
npm packages
Files it writes
More from beui
All 104 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Action Swap Bluraction-swap-blur | beui | Components | Free | 3 deps · 4 files | |
| Action Swap Cascadeaction-swap-cascade | beui | Components | Free | 3 deps · 4 files | |
| Action Swap Rollaction-swap-roll | beui | Components | Free | 3 deps · 4 files | |
| Agent Activityagent-activity | beui | Components | Free | 4 deps · 9 files | |
| Agent Loading States Agent Progressagent-progress | beui | Components | Free | 3 deps · 3 files | |
| AI Sidebarai-sidebar | beui | Components | Free | 4 deps · 5 files | |
| Animated Badgeanimated-badge | beui | Components | Free | 4 deps · 3 files | |
| Number Animation Animated Numberanimated-number | beui | Components | Free | 3 deps · 3 files |
