1'use client';23import { Button } from '@/components/ui/button';4import { Sheet } from '@/components/ui/sheet';56export function Default() {7 return (8 <Sheet>9 <Sheet.Trigger>Open Sheet</Sheet.Trigger>10 <Sheet.Content side="right" size="sm">11 <div className="flex flex-col gap-4">12 <h2 className="text-lg font-semibold">Notifications</h2>13 <p className="text-muted-foreground text-sm">14 You have 3 new messages and 1 system alert. Review them below.15 </p>1617 <div className="space-y-2 text-sm">18 <div className="bg-accent/10 rounded-md border p-3">19 <strong>Message from Jane:</strong> Your report is ready for download.20 </div>21 <div className="bg-accent/10 rounded-md border p-3">22 <strong>System Alert:</strong> Scheduled maintenance at 3:00 AM UTC.23 </div>24 <div className="bg-accent/10 rounded-md border p-3">25 <strong>Message from John:</strong> Please review the updated project plan.26 </div>27 </div>2829 <div className="mt-4 flex justify-end gap-2">30 <Sheet.Close>31 <Button variant="outline">Dismiss All</Button>32 </Sheet.Close>33 <Button>View Details</Button>34 </div>35 </div>36 </Sheet.Content>37 </Sheet>38 );39}
Installation
Copy and paste the following code in your project.
'use client';import { Cancel01Icon } from '@hugeicons/core-free-icons';import { HugeiconsIcon } from '@hugeicons/react';import { cva, type VariantProps } from 'class-variance-authority';import {AnimatePresence,HTMLMotionProps,motion,useMotionValue,useTransform,} from 'motion/react';import * as React from 'react';import { createPortal } from 'react-dom';import { cn } from '../lib/cn';import { Button } from './button';// --- Animation constants (module level) ---const slideVariants = {bottom: {initial: { y: '100%' },animate: { y: 0 },exit: { y: '110%' },},top: {initial: { y: '-100%' },animate: { y: 0 },exit: { y: '-110%' },},left: {initial: { x: '-100%' },animate: { x: 0 },exit: { x: '-110%' },},right: {initial: { x: '100%' },animate: { x: 0 },exit: { x: '110%' },},} as const;const SHEET_SPRING = { type: 'spring', damping: 32, stiffness: 320 } as const;const SHEET_OVERLAY_VARIANTS = {initial: { opacity: 0 },animate: { opacity: 1 },exit: { opacity: 0 },} as const;const SHEET_OVERLAY_TRANSITION = { duration: 0.2 } as const;const SHEET_OVERLAY_STYLE = { willChange: 'opacity' } as const;const SHEET_CONTENT_STYLE = { willChange: 'transform' } as const;const CLOSE_BUTTON_TAP = { scale: 0.9 } as const;const SWIPE_CLOSE_THRESHOLD = 80;// --- Context ---type SheetContextProps = {open: boolean;setOpen: React.Dispatch<React.SetStateAction<boolean>>;id: string;};const SheetContext = React.createContext<SheetContextProps | null>(null);function useSheetContext() {const ctx = React.use(SheetContext);if (!ctx) throw new Error('Sheet components must be inside <Sheet>.');return ctx;}// --- Components ---const SheetRoot = ({ children }: { children: React.ReactNode }) => {const [open, setOpen] = React.useState(false);const id = React.useId();React.useEffect(() => {document.body.style.overflow = open ? 'hidden' : 'unset';return () => {document.body.style.overflow = 'unset';};}, [open]);return <SheetContext value={{ open, setOpen, id }}>{children}</SheetContext>;};const SheetTrigger = ({children,className,}: {children: React.ReactNode;className?: string;}) => {const { setOpen } = useSheetContext();return (<Button className={className} onClick={() => setOpen(true)}>{children}</Button>);};const SheetOverlay = ({ className }: { className?: string }) => {const { setOpen } = useSheetContext();return (<motion.divvariants={SHEET_OVERLAY_VARIANTS}initial="initial"animate="animate"exit="exit"transition={SHEET_OVERLAY_TRANSITION}style={SHEET_OVERLAY_STYLE}className={cn('fixed inset-0 z-300 bg-black/50 backdrop-blur-xs', className)}onClick={() => setOpen(false)}/>);};// --- CVA ---const sheetVariants = cva('fixed z-300 bg-background border shadow-2xl overflow-auto rounded-2xl', {variants: {side: {bottom: 'bottom-3 left-3 right-3',top: 'top-3 left-3 right-3',left: 'left-3 top-3 bottom-3',right: 'right-3 top-3 bottom-3',},size: {sm: 'sm:left-auto sm:w-80',md: 'sm:left-auto sm:w-96',lg: 'sm:left-auto sm:w-[28rem]',full: 'w-full h-full !rounded-none !inset-0',},},defaultVariants: {side: 'bottom',size: 'md',},});interface SheetContentPropsextends Omit<HTMLMotionProps<'div'>, 'children'>, VariantProps<typeof sheetVariants> {children: React.ReactNode;showDragHandle?: boolean;}const SheetContent = ({children,className,side = 'bottom',size,showDragHandle = true,...props}: SheetContentProps) => {const { open, setOpen, id } = useSheetContext();const [mounted, setMounted] = React.useState(false);const dragY = useMotionValue(0);const dragX = useMotionValue(0);const overlayOpacityFromY = useTransform(dragY, [0, 200], [1, 0]);const overlayOpacityFromX = useTransform(dragX, [0, 200], [1, 0]);React.useEffect(() => setMounted(true), []);React.useEffect(() => {if (!open) return;const handleKeyDown = (e: KeyboardEvent) => {if (e.key === 'Escape') setOpen(false);};document.addEventListener('keydown', handleKeyDown);return () => document.removeEventListener('keydown', handleKeyDown);}, [open, setOpen]);const isVertical = side === 'bottom' || side === 'top';const isHorizontal = side === 'left' || side === 'right';const dragAxis = isVertical ? 'y' : 'x';const dragConstraints = React.useMemo(() => {if (side === 'bottom') return { top: 0, bottom: 0 };if (side === 'top') return { top: 0, bottom: 0 };if (side === 'left') return { left: 0, right: 0 };if (side === 'right') return { left: 0, right: 0 };return {};}, [side]);const dragElastic = 0.15;const handleDragEnd = (_: unknown, info: { offset: { x: number; y: number } }) => {const { x, y } = info.offset;const shouldClose =(side === 'bottom' && y > SWIPE_CLOSE_THRESHOLD) ||(side === 'top' && -y > SWIPE_CLOSE_THRESHOLD) ||(side === 'right' && x > SWIPE_CLOSE_THRESHOLD) ||(side === 'left' && -x > SWIPE_CLOSE_THRESHOLD);if (shouldClose) {setOpen(false);} else {// Snap backif (isVertical) dragY.set(0);if (isHorizontal) dragX.set(0);}};if (!mounted) return null;if (!side) return null;const sheet = (<AnimatePresence>{open && (<><SheetOverlay className="z-300" /><motion.divrole="dialog"aria-modal="true"aria-labelledby={`${id}-title`}aria-describedby={`${id}-description`}drag={dragAxis}dragMomentum={false}dragElastic={dragElastic}dragConstraints={dragConstraints}onDragEnd={handleDragEnd}style={{...(isVertical ? { y: dragY } : { x: dragX }),...SHEET_CONTENT_STYLE,}}initial={slideVariants[side].initial}animate={slideVariants[side].animate}exit={slideVariants[side].exit}transition={SHEET_SPRING}className={cn(sheetVariants({ side, size }), className)}{...props}>{showDragHandle && (<div className="flex shrink-0 items-center justify-center py-2.5"><div className="bg-muted-foreground/30 h-1 w-10 cursor-grab rounded-full active:cursor-grabbing" /></div>)}{/* Close button */}<div className="flex justify-end px-4 pt-2 pb-0"><motion.buttonwhileTap={CLOSE_BUTTON_TAP}onClick={() => setOpen(false)}className="hover:bg-muted rounded-full p-2 transition-colors"aria-label="Close"><HugeiconsIcon icon={Cancel01Icon} className="h-4 w-4" size={16} /></motion.button></div>{/* Content */}<div className="px-5 pt-2 pb-5">{children}</div></motion.div></>)}</AnimatePresence>);return createPortal(sheet, document.body);};const SheetClose = ({ children }: { children: React.ReactNode }) => {const { setOpen } = useSheetContext();return (<button type="button" onClick={() => setOpen(false)}>{children}</button>);};const SheetHeader = ({children,className,}: {children: React.ReactNode;className?: string;}) => {return <div className={cn('mb-4', className)}>{children}</div>;};const SheetTitle = ({ children, className }: { children: React.ReactNode; className?: string }) => {const { id } = useSheetContext();return (<h2 id={`${id}-title`} className={cn('text-xl font-semibold tracking-tight', className)}>{children}</h2>);};const SheetDescription = ({children,className,}: {children: React.ReactNode;className?: string;}) => {const { id } = useSheetContext();return (<p id={`${id}-description`} className={cn('text-muted-foreground mt-1 text-sm', className)}>{children}</p>);};const Sheet = Object.assign(SheetRoot, {Trigger: SheetTrigger,Content: SheetContent,Header: SheetHeader,Title: SheetTitle,Description: SheetDescription,Close: SheetClose,});export { Sheet };
Make sure to update the import paths according to your project structure.
Anatomy
import { Sheet } from '@/components/ui/sheet';
<Sheet><Sheet.Trigger><Button>Open Sheet</Button></Sheet.Trigger><Sheet.Content><Sheet.Header><Sheet.Title>Sheet Title</Sheet.Title><Sheet.Description>This is a sheet component.</Sheet.Description></Sheet.Header><div>Your content here</div></Sheet.Content></Sheet>
Features
- Multiple sides - Slide from bottom, top, left, or right
- Drag to close - Swipe down/up to dismiss (on bottom/top sheets)
- Responsive - Adapts to different screen sizes
- Smooth animations - Spring-based slide transitions
API Reference
Sheet
Root component that manages sheet state.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Sheet trigger and content |
SheetTrigger
Button that opens the sheet.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Trigger button content |
className | string | - | Additional CSS classes |
SheetContent
The sheet content container with slide animations.
| Prop | Type | Default | Description |
|---|---|---|---|
side | 'bottom' | 'top' | 'left' | 'right' | 'bottom' | Side to slide from |
size | 'sm' | 'md' | 'lg' | 'full' | 'md' | Sheet size |
showDragHandle | boolean | true | Show drag handle for closing |
className | string | - | Additional CSS classes |
SheetHeader
Container for sheet title and description.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
SheetTitle
The sheet title heading.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
SheetDescription
Optional description text for the sheet.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
SheetClose
Wrapper that closes the sheet when clicked.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Clickable content |