Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/sheet.jsonInstalls to @/components/ui/sheet.
Manual installation
Install the dependencies:
pnpm add @base-ui/react@^1.8.0 class-variance-authority@^0.7.1 lucide-react@^1.48.0Add the components it builds on: button.
Copy the source into your project:
'use client'
import { Dialog as SheetPrimitive } from '@base-ui/react/dialog'
import { cva, type VariantProps } from 'class-variance-authority'
import { XIcon } from 'lucide-react'
import type * as React from 'react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
return (
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
'fixed inset-0 z-50 bg-black/50 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs',
className,
)}
{...props}
/>
)
}
// Plain classes per side, not data-[side=…] variants: the attribute selector out-ranks a caller's className, so a
// drawer could not set its own width
const sheetContentVariants = cva(
'fixed z-50 flex flex-col gap-4 overflow-y-auto bg-background bg-clip-padding text-sm text-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0',
{
variants: {
side: {
top: 'inset-x-0 top-0 h-auto max-h-dvh border-b data-ending-style:translate-y-[-2.5rem] data-starting-style:translate-y-[-2.5rem]',
right:
'inset-y-0 right-0 h-full w-3/4 border-l data-ending-style:translate-x-[2.5rem] data-starting-style:translate-x-[2.5rem] sm:max-w-sm',
bottom:
'inset-x-0 bottom-0 h-auto max-h-dvh border-t data-ending-style:translate-y-[2.5rem] data-starting-style:translate-y-[2.5rem]',
left:
'inset-y-0 left-0 h-full w-3/4 border-r data-ending-style:translate-x-[-2.5rem] data-starting-style:translate-x-[-2.5rem] sm:max-w-sm',
},
},
defaultVariants: {
side: 'right',
},
},
)
function SheetContent({
className,
children,
side = 'right',
showCloseButton = true,
...props
}: SheetPrimitive.Popup.Props & {
side?: NonNullable<VariantProps<typeof sheetContentVariants>['side']>
showCloseButton?: boolean
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Popup
data-slot="sheet-content"
data-side={side}
className={cn(sheetContentVariants({ side }), className)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close
data-slot="sheet-close"
render={<Button variant="ghost" className="absolute top-3 right-3" size="icon-sm" />}
>
<XIcon />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Popup>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="sheet-header" className={cn('flex flex-col gap-0.5 p-4', className)} {...props} />
}
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="sheet-footer" className={cn('mt-auto flex flex-col gap-2 p-4', className)} {...props} />
}
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn('font-heading text-base font-medium text-foreground', className)}
{...props}
/>
)
}
function SheetDescription({ className, ...props }: SheetPrimitive.Description.Props) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
)
}
export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription }A sheet component that slides in from the edge of the screen. It can be triggered from any side of the viewport.
Features
- Can be opened from any edge of the screen (top, right, bottom, left)
- Supports controlled and uncontrolled states
- Keyboard accessible
- Focus management
- Screen reader announcements
- Slide and fade animations with a blurred backdrop
- Built-in close button (toggle with
showCloseButton) - Responsive design
Usage
import {
Sheet,
SheetTrigger,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
SheetClose,
} from "@/components/ui/sheet"
import { Button } from "@/components/ui/button"
export function ContactDetailsSheet() {
return (
<Sheet>
<SheetTrigger render={<Button variant="outline" />}>Edit contact details</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Contact details</SheetTitle>
<SheetDescription>We use these details to reach you about your cases.</SheetDescription>
</SheetHeader>
<div className="px-4">{/* Your content */}</div>
<SheetFooter>
<Button type="submit">Save changes</Button>
<SheetClose render={<Button variant="outline" />}>Close</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
)
}Examples
Basic Sheet
A resident's contact details, edited in a sheet from the right.
Position Variants
The sheet can slide in from different sides of the screen.
Top Sheet
A service notice about planned maintenance.
Bottom Sheet
Actions for a decision, like a mobile action sheet.
Left Sheet
Navigation for My pages, opened from an icon-only menu button.
Controlled Sheet
Selecting a case in the list opens its details. The sheet's open state follows the selected case.
Without Close Button
A filter sheet with the corner close button hidden with showCloseButton={false}. It closes with Escape or the footer buttons.
Custom Width
A translations sheet that needs more room. className="w-full sm:max-w-lg" replaces the default w-3/4 sm:max-w-sm: full width on phones, 512px from sm up.
Anatomy
<Sheet>
<SheetTrigger />
<SheetContent>
<SheetHeader>
<SheetTitle />
<SheetDescription />
</SheetHeader>
<SheetFooter>
<SheetClose />
</SheetFooter>
</SheetContent>
</Sheet>Props
Sheet
The root component that manages the sheet's state.
SheetTrigger
The button that opens the sheet. By default, it's a button element.
<SheetTrigger>Open Sheet</SheetTrigger>SheetContent
The component that contains the sheet's content. It can be positioned on any side of the screen.
<SheetContent side="right">
{/* Your content */}
</SheetContent>Props:
side: "top" | "right" | "bottom" | "left" (default "right")showCloseButton: Whether to render the close button in the top-right corner (defaulttrue)className: Additional CSS classes. Left and right sheets arew-3/4withsm:max-w-smby default; set your own width here to replace it, e.g.w-full sm:max-w-lg
Header and footer have their own p-4 padding; wrap body content in e.g. px-4.
SheetHeader
A container for the sheet's header content.
<SheetHeader>
<SheetTitle>Sheet Title</SheetTitle>
<SheetDescription>Sheet Description</SheetDescription>
</SheetHeader>SheetFooter
A container for the sheet's footer content.
<SheetFooter>
<Button>Save changes</Button>
</SheetFooter>SheetClose
Closes the sheet. Use the render prop to render your own button.
<SheetClose render={<Button variant="outline" />}>Close</SheetClose>SheetTitle
The title of the sheet. It is automatically announced by screen readers.
<SheetTitle>Contact details</SheetTitle>SheetDescription
The description of the sheet. It is automatically announced by screen readers.
<SheetDescription>
We use these details to reach you about your cases.
</SheetDescription>Accessibility
The Sheet component follows WAI-ARIA guidelines for dialogs and modals:
- Uses appropriate ARIA roles and attributes
- Manages focus when opened and closed
- Supports keyboard navigation (Escape to close)
- Provides screen reader announcements
- Implements a focus trap when opened
Keyboard Interactions
SpaceorEnteron trigger: Opens the sheetEscape: Closes the sheetTab: Moves focus through interactive elementsShift + Tab: Moves focus through interactive elements in reverse
Best Practices
- Always provide descriptive titles and descriptions
- Use appropriate side positioning based on content context
- Implement proper focus management
- Consider mobile responsiveness when choosing side positions
- Keep content concise and focused
Related Components
- Dialog - For centered modal dialogs
- Drawer - For touch-friendly sliding panels
- Sidebar - Uses Sheet for its mobile layout
- Popover - For smaller floating content