Mobile Drawer
fluid-functionalism/mobile-drawerFreeComponent
Slide-in navigation drawer with scrim, focus trap, and scroll lock. Radix flavor.
Install it
npx shadcn@latest add https://fluidfunctionalism.com/r/mobile-drawer.jsonSource
1"use client";23import { useEffect, useState, type ReactNode, type RefObject } from "react";4import * as DialogPrimitive from "@radix-ui/react-dialog";5import { motion } from "framer-motion";6import { spring, exitFallbackMs } from "@/lib/springs";7import { useSurface, SurfaceProvider } from "@/lib/surface-context";8import { surfaceClasses } from "@/lib/surface-classes";910// Built on Radix Dialog: it provides scroll lock, focus trap, focus restore,11// Esc + outside-click dismissal, while leaving the slide animation to12// framer-motion. Radix has no actionsRef-style deferred unmount, so the13// portal lifetime is managed with local `mounted` state (the same pattern14// the Dialog component uses): mount on open, keep the portal alive with15// `forceMount` through the exit tween, and unmount once the panel's exit16// animation completes.1718interface MobileDrawerProps {19 open: boolean;20 onClose: () => void;21 children: ReactNode;22 triggerRef?: RefObject<HTMLElement | null>;23}2425export function MobileDrawer({26 open,27 onClose,28 children,29 triggerRef,30}: MobileDrawerProps) {31 const substrate = useSurface();32 const level = Math.min(substrate + 2, 8);3334 // Portal lifetime: mounts as soon as `open` flips true; on close it stays35 // mounted (forceMount below) until the exit tween finishes.36 const [mounted, setMounted] = useState(false);3738 useEffect(() => {39 if (open) setMounted(true);40 }, [open]);4142 // Fallback release for the deferred unmount: onAnimationComplete on the43 // panel is the primary signal, but rAF-driven animation callbacks can44 // stall in throttled/background tabs. The longest exit tween is45 // spring.moderate.exit (backdrop), so the fallback tracks that tier's exit46 // duration plus a safety buffer.47 useEffect(() => {48 if (open) return;49 const id = setTimeout(() => setMounted(false), exitFallbackMs(spring.moderate));50 return () => clearTimeout(id);51 }, [open]);5253 return (54 <DialogPrimitive.Root55 open={open}56 onOpenChange={(nextOpen) => {57 if (!nextOpen) onClose();58 }}59 >60 {mounted && (61 <DialogPrimitive.Portal forceMount>62 {/* Overlay — same scrim as the library's dialogs: an always-on63 bg-black/40 base that stays visible for system-dark users (the64 `dark:` variant only matches the explicit .dark class), boosted65 to /80 in explicit dark mode. */}66 <DialogPrimitive.Overlay asChild forceMount>67 <motion.div68// … truncated
What it pulls in
npm packages
Other registry items
Files it writes
More from fluid-functionalism
All 53 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Accordionaccordion | fluid-functionalism | Components | Free | 3 deps | |
| Accordion (Base UI)accordion-base | fluid-functionalism | Components | Free | 3 deps | |
| Ask User Questionsask-user-questions | fluid-functionalism | Components | Free | 2 deps | |
| Badgebadge | fluid-functionalism | Components | Free | 1 dep | |
| Buttonbutton | fluid-functionalism | Components | Free | 4 deps | |
| Button (Base UI)button-base | fluid-functionalism | Components | Free | 4 deps | |
| Cardcard | fluid-functionalism | Components | Free | 1 dep | |
| Chat Messagechat-message | fluid-functionalism | Components | Free | 1 dep |
