A sheet is a panel that slides in from the edge of the screen. It’s useful for displaying navigation menus, filters, settings, or any content that should be accessible but not always visible.
Installation
npx shadcn@latest add @eo-n/sheet
Install dependencies
npm install @base-ui/react lucide-react
Copy component code
Copy and paste the following code into components/ui/sheet.tsx:"use client";
import * as React from "react";
import { Dialog as SheetPrimitive } from "@base-ui/react";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root;
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetBackdrop({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Backdrop>) {
return (
<SheetPrimitive.Backdrop
data-slot="sheet-backdrop"
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-[1.5px] transition-opacity duration-150 ease-out data-[ending-style]:opacity-0 data-[starting-style]:opacity-0",
className
)}
{...props}
/>
);
}
interface SheetContentProps
extends React.ComponentProps<typeof SheetPrimitive.Popup> {
hideCloseIcon?: boolean;
side?: "top" | "right" | "bottom" | "left";
flush?: boolean;
}
function SheetContent({
className,
children,
side = "right",
hideCloseIcon = false,
flush = false,
...props
}: SheetContentProps) {
return (
<SheetPortal>
<SheetBackdrop />
<SheetPrimitive.Popup
data-slot="sheet-content"
data-side={side}
data-flush={flush}
className={cn(
"group bg-background fixed z-50 flex flex-col gap-4 p-6 shadow-lg transition-all ease-out data-[closed]:duration-500 data-[open]:duration-300",
"data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l sm:data-[side=right]:max-w-sm",
"data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r sm:data-[side=left]:max-w-sm",
"data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b",
"data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t",
flush && "gap-0 p-0",
className
)}
{...props}
>
{children}
{!hideCloseIcon && (
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100">
<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-2",
"group-data-[flush=true]:mb-6 group-data-[flush=true]:border-b group-data-[flush=true]:p-6",
className
)}
{...props}
/>
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn(
"mt-auto flex flex-col gap-2",
"group-data-[flush=true]:bg-muted/60 group-data-[flush=true]:border-t group-data-[flush=true]:p-6",
className
)}
{...props}
/>
);
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetBackdrop,
SheetFooter,
SheetTitle,
SheetDescription,
};
Update imports
Update the import paths to match your project setup.
Import all parts and piece them together:
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
<Sheet>
<SheetTrigger>Open</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Are you sure you want to proceed?</SheetTitle>
<SheetDescription>
This action may have permanent effects. Please confirm if you want to
continue.
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
Examples
Basic Sheet
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
export default function SheetDemo() {
return (
<Sheet>
<SheetTrigger asChild>
<Button>Open Sheet</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Sheet Title</SheetTitle>
<SheetDescription>
This is a sheet that slides in from the right side of the screen.
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
);
}
Different Sides
Control which side the sheet slides in from using the side prop:
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
export default function SheetSides() {
return (
<div className="flex gap-4">
<Sheet>
<SheetTrigger asChild>
<Button>Left</Button>
</SheetTrigger>
<SheetContent side="left">
<SheetHeader>
<SheetTitle>Left Sheet</SheetTitle>
<SheetDescription>
This sheet slides in from the left.
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
<Sheet>
<SheetTrigger asChild>
<Button>Right</Button>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Right Sheet</SheetTitle>
<SheetDescription>
This sheet slides in from the right.
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
<Sheet>
<SheetTrigger asChild>
<Button>Top</Button>
</SheetTrigger>
<SheetContent side="top">
<SheetHeader>
<SheetTitle>Top Sheet</SheetTitle>
<SheetDescription>
This sheet slides in from the top.
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
<Sheet>
<SheetTrigger asChild>
<Button>Bottom</Button>
</SheetTrigger>
<SheetContent side="bottom">
<SheetHeader>
<SheetTitle>Bottom Sheet</SheetTitle>
<SheetDescription>
This sheet slides in from the bottom.
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
</div>
);
}
Add action buttons in a footer:
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
export default function SheetWithFooter() {
return (
<Sheet>
<SheetTrigger asChild>
<Button>Edit Profile</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Edit Profile</SheetTitle>
<SheetDescription>
Make changes to your profile here. Click save when you're done.
</SheetDescription>
</SheetHeader>
<div className="flex-1">
{/* Form content */}
</div>
<SheetFooter>
<SheetClose asChild>
<Button variant="outline">Cancel</Button>
</SheetClose>
<Button>Save Changes</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
Control the sheet state to open it from a dropdown menu:
import * as React from "react";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
export default function SheetFromMenu() {
const menuTriggerRef = React.useRef<HTMLButtonElement>(null);
const [sheetOpen, setSheetOpen] = React.useState(false);
return (
<>
<DropdownMenu>
<DropdownMenuTrigger ref={menuTriggerRef} asChild>
<Button>Open Menu</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setSheetOpen(true)}>
Open Sheet
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<SheetContent finalFocus={menuTriggerRef}>
<SheetHeader>
<SheetTitle>Sheet from Menu</SheetTitle>
<SheetDescription>
This sheet was opened from a menu item. Focus will return to
the menu trigger when closed.
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
</>
);
}
Make sure to use the sheet’s finalFocus prop to return focus back to the menu trigger for proper accessibility.
Use a sheet for mobile navigation:
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { Menu } from "lucide-react";
export default function NavigationSheet() {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon">
<Menu className="h-5 w-5" />
<span className="sr-only">Toggle menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left">
<SheetHeader>
<SheetTitle>Navigation</SheetTitle>
</SheetHeader>
<nav className="flex flex-col gap-4 mt-6">
<a href="#" className="text-lg font-medium hover:underline">
Home
</a>
<a href="#" className="text-lg font-medium hover:underline">
About
</a>
<a href="#" className="text-lg font-medium hover:underline">
Services
</a>
<a href="#" className="text-lg font-medium hover:underline">
Contact
</a>
</nav>
</SheetContent>
</Sheet>
);
}
API Reference
The root component that manages the sheet state.
Controls the open state of the sheet.
The initial open state for uncontrolled usage.
Callback fired when the open state changes.
SheetTrigger
The button that opens the sheet.
Merge props onto the child element instead of wrapping it.
SheetContent
Contains the content to be rendered in the open sheet.
side
'top' | 'right' | 'bottom' | 'left'
default:"'right'"
The side of the screen from which the sheet will slide in.
Hides the X close icon button.
Removes padding and gap from the content container. Useful when using SheetHeader and SheetFooter with borders.
finalFocus
React.RefObject<HTMLElement>
Element to receive focus when the sheet closes.
Additional CSS classes to apply.
Wrapper for the sheet title and description.
Additional CSS classes to apply.
Wrapper for sheet action buttons.
Additional CSS classes to apply.
SheetTitle
The accessible title of the sheet.
Additional CSS classes to apply.
SheetDescription
The accessible description of the sheet.
Additional CSS classes to apply.
SheetClose
A button that closes the sheet.
Merge props onto the child element instead of wrapping it.