Mobile Drawer
nexvyn/mobile-drawerFreeComponent
A bottom-sheet drawer with swipe-to-dismiss, focus trap, and scroll lock.
Live preview
live · rendered by the registry
Rendered by nexvyn on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://ui.nexvyn.dev/r/mobile-drawer.jsonSource
1'use client'23import {4 forwardRef,5 useCallback,6 useEffect,7 useId,8 useRef,9 useState,10 type HTMLAttributes,11 type ReactNode,12} from 'react'13import { createPortal } from 'react-dom'14import {15 AnimatePresence,16 motion,17 useMotionValue,18 useReducedMotion,19 useTransform,20} from 'motion/react'21import { cn } from '@/lib/utils'22import { springs } from '@/lib/motion-tokens'23import { useFocusTrap } from '@/lib/hooks/use-focus-trap'24import { useMounted } from '@/hooks/use-mounted'2526export interface MobileDrawerProps extends HTMLAttributes<HTMLDivElement> {27 open: boolean28 onClose: () => void29 children: ReactNode30 triggerRef?: React.RefObject<HTMLElement | null>31 dismissible?: boolean32}3334export interface MobileDrawerTitleProps extends HTMLAttributes<HTMLHeadingElement> {35 children: ReactNode36}3738export const MobileDrawerTitle = forwardRef<HTMLHeadingElement, MobileDrawerTitleProps>(39 ({ children, className, ...props }, ref) => (40 <h241 ref={ref}42 className={cn('px-6 pt-6 pb-2 text-lg font-semibold text-(--color-fg)', className)}43 {...props}44 >45 {children}46 </h2>47 ),48)49MobileDrawerTitle.displayName = 'MobileDrawerTitle'5051export const MobileDrawer = forwardRef<HTMLDivElement, MobileDrawerProps>(52 (53 {54 open,55 onClose,56 children,57 triggerRef: triggerRefProp,58 dismissible = true,59 className,60 ...props61 },62 ref,63 ) => {64 const reduceMotion = useReducedMotion()65 const mounted = useMounted()66 const panelRef = useRef<HTMLDivElement>(null)67 const dialogRef = useRef<HTMLDivElement>(null)68 const previousScrollRef = useRef<string>('')69 const dragY = useMotionValue(0)70 const overlayOpacity = useTransform(dragY, [0, 300], [0.8, 0])7172 const reactId = useId()73 const titleId = `${reactId}-drawer-title`7475 useFocusTrap(dialogRef, open, triggerRefProp)7677 useEffect(() => {78 if (!open) return79 previousScrollRef.current = document.body.style.overflow80 document.body.style.overflow = 'hidden'81 return () => {82 document.body.style.overflow = previousScrollRef.current83 }84 }, [open])8586 const handleDragEnd = useCallback(87 (_: unknown, info: { offset: { y: number }; velocity: { y: number } }) => {88 if (!dismissible) return89 const panelHeight = panelRef.current?.offsetHeight ?? 30090 const pastThreshold = info.offset.y > panelHeight * 0.391 const fastFlick = info.velocity.y > 50092// … truncated
What it pulls in
npm packages
Files it writes
More from nexvyn
All 33 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Badgebadge | nexvyn | Components | Free | 2 deps · 2 files | |
| Bars Themebars-theme | nexvyn | Components | Free | 1 dep · 3 files | |
| Bounce Sidebarbounce-sidebar | nexvyn | Components | Free | 1 dep · 2 files | |
| Breadcrumbsbreadcrumbs | nexvyn | Components | Free | 1 dep · 2 files | |
| Checkboxcheckbox | nexvyn | Components | Free | 1 dep · 3 files | |
| Clipboard Fieldclipboard-field | nexvyn | Components | Free | no deps · 2 files | |
| Color Pickercolor-picker | nexvyn | Components | Free | no deps · 16 files | |
| Comboboxcombobox | nexvyn | Components | Free | 1 dep · 2 files |
