Shadcn Sheet
Free shadcn/ui sheet component for React — a side panel on Radix Dialog. Slide in from any edge, mobile navigation, forms, custom widths, and a scrollable cart drawer. The examples official shadcn never shipped.
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
Props
side
= "right"
"top" | "right" | "bottom" | "left"
Which edge the panel slides in from (on SheetContent).
open
= —
boolean
Controlled open state (on Sheet). Pair with onOpenChange.
onOpenChange
= —
(open: boolean) => void
Fires when the sheet opens or closes (on Sheet).
Eight parts (Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription) built on Radix Dialog. SheetContent has a built-in close X and a side prop (top/right/bottom/left, default right). Set the width with a className on SheetContent (default sm:max-w-sm). The slide/fade animations require tw-animate-css (or tailwindcss-animate) in your CSS.
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.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
}: 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}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
Examples
Basic
A sheet that slides in from the right with a `SheetHeader`, body, and `SheetFooter` — the canonical side panel.
Packages
Props
No props documented yet.
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
export default function SheetDemo01() {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">Open sheet</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Notifications</SheetTitle>
<SheetDescription>
Choose how you want to be notified. Changes are saved automatically.
</SheetDescription>
</SheetHeader>
<div className="px-4 text-sm text-muted-foreground">
A sheet slides in from the edge of the screen — use it for secondary
content that complements the main view without leaving the page.
</div>
<SheetFooter>
<SheetClose asChild>
<Button>Done</Button>
</SheetClose>
<SheetClose asChild>
<Button variant="outline">Cancel</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
)
}
Four sides
Open the panel from any edge — `top`, `right`, `bottom`, or `left` — with the `side` prop on `SheetContent`.
Packages
Props
No props documented yet.
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
const sides = ["top", "right", "bottom", "left"] as const
export default function SheetSides01() {
return (
<div className="flex flex-wrap items-center gap-3">
{sides.map((side) => (
<Sheet key={side}>
<SheetTrigger asChild>
<Button variant="outline" className="capitalize">
{side}
</Button>
</SheetTrigger>
<SheetContent side={side}>
<SheetHeader>
<SheetTitle className="capitalize">{side} sheet</SheetTitle>
<SheetDescription>
This panel slides in from the {side}. Set it with the{" "}
<code>side</code> prop on <code>SheetContent</code>.
</SheetDescription>
</SheetHeader>
<SheetFooter>
<SheetClose asChild>
<Button variant="outline">Close</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
))}
</div>
)
}
Mobile navigation
A hamburger-triggered **left** sheet whose links close the menu on click — the canonical responsive nav drawer.
Packages
Props
No props documented yet.
import { MenuIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetClose,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
const links = [
{ label: "Dashboard", href: "#" },
{ label: "Projects", href: "#" },
{ label: "Team", href: "#" },
{ label: "Settings", href: "#" },
]
export default function SheetNav01() {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline" size="icon" aria-label="Open menu">
<MenuIcon />
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-72">
<SheetHeader>
<SheetTitle>Acme Inc.</SheetTitle>
</SheetHeader>
<nav className="grid gap-1 px-2">
{links.map((link) => (
<SheetClose asChild key={link.label}>
<a
href={link.href}
className="rounded-md px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
>
{link.label}
</a>
</SheetClose>
))}
</nav>
<SheetFooter>
<Button>Upgrade plan</Button>
</SheetFooter>
</SheetContent>
</Sheet>
)
}
With a form
A **controlled** sheet holding an edit form that closes itself on submit via `setOpen(false)`.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
export default function SheetForm01() {
const [open, setOpen] = React.useState(false)
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button variant="outline">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>
<form
onSubmit={(event) => {
event.preventDefault()
setOpen(false)
}}
className="flex flex-1 flex-col gap-4 px-4"
>
<div className="grid gap-2">
<Label htmlFor="sheet-name">Name</Label>
<Input id="sheet-name" defaultValue="Ada Lovelace" />
</div>
<div className="grid gap-2">
<Label htmlFor="sheet-username">Username</Label>
<Input id="sheet-username" defaultValue="@ada" />
</div>
<div className="mt-auto flex flex-col gap-2 pb-4">
<Button type="submit">Save changes</Button>
<SheetClose asChild>
<Button type="button" variant="outline">
Cancel
</Button>
</SheetClose>
</div>
</form>
</SheetContent>
</Sheet>
)
}
Widths
Resize the panel with a `max-width` utility on `SheetContent` — `sm:max-w-md`, `sm:max-w-lg`, or a full `w-[90vw]`.
Packages
Props
No props documented yet.
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
const sizes = [
{ label: "Small", className: "sm:max-w-sm" },
{ label: "Medium", className: "sm:max-w-md" },
{ label: "Large", className: "sm:max-w-lg" },
{ label: "Wide", className: "w-[90vw] sm:max-w-3xl" },
]
export default function SheetSizes01() {
return (
<div className="flex flex-wrap items-center gap-3">
{sizes.map((size) => (
<Sheet key={size.label}>
<SheetTrigger asChild>
<Button variant="outline">{size.label}</Button>
</SheetTrigger>
<SheetContent className={size.className}>
<SheetHeader>
<SheetTitle>{size.label} sheet</SheetTitle>
<SheetDescription>
The default is <code>sm:max-w-sm</code>. Override the width with a
utility class on <code>SheetContent</code> — this one uses{" "}
<code>{size.className}</code>.
</SheetDescription>
</SheetHeader>
<SheetFooter>
<SheetClose asChild>
<Button>Close</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
))}
</div>
)
}
Scrollable
A sticky header and footer with a scrollable body — `flex-1 overflow-y-auto` on the middle region for long content.
Packages
Props
No props documented yet.
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetClose,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
const events = Array.from({ length: 20 }, (_, i) => i + 1)
export default function SheetScroll01() {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">View activity</Button>
</SheetTrigger>
<SheetContent className="gap-0 p-0">
<SheetHeader className="border-b">
<SheetTitle>Activity</SheetTitle>
</SheetHeader>
<div className="flex-1 overflow-y-auto p-4">
<div className="grid gap-3">
{events.map((n) => (
<div
key={n}
className="rounded-md border border-border p-3 text-sm"
>
<p className="font-medium">Event #{n}</p>
<p className="text-muted-foreground">
Something happened a little while ago.
</p>
</div>
))}
</div>
</div>
<SheetFooter className="border-t">
<SheetClose asChild>
<Button>Close</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
)
}
Shopping cart
A right-side cart drawer with line items and a sticky checkout footer — the e-commerce mini-cart pattern.
Packages
Props
No props documented yet.
import { ShoppingCartIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetClose,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
const cart = [
{ name: "Wireless headphones", price: 199, qty: 1 },
{ name: "USB-C cable", price: 19, qty: 2 },
{ name: "Laptop stand", price: 49, qty: 1 },
]
export default function SheetCart01() {
const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0)
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">
<ShoppingCartIcon />
Cart ({cart.length})
</Button>
</SheetTrigger>
<SheetContent className="gap-0 p-0">
<SheetHeader className="border-b">
<SheetTitle>Your cart</SheetTitle>
</SheetHeader>
<div className="flex-1 overflow-y-auto p-4">
<ul className="grid gap-4">
{cart.map((item) => (
<li
key={item.name}
className="flex items-center justify-between gap-3 text-sm"
>
<div>
<p className="font-medium">{item.name}</p>
<p className="text-muted-foreground">Qty {item.qty}</p>
</div>
<span className="font-medium tabular-nums">
${item.price * item.qty}
</span>
</li>
))}
</ul>
</div>
<SheetFooter className="border-t">
<div className="flex items-center justify-between text-sm font-medium">
<span>Total</span>
<span className="tabular-nums">${total}</span>
</div>
<Button>Checkout</Button>
<SheetClose asChild>
<Button variant="outline">Continue shopping</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
)
}
The Shadcn Sheet is a panel that slides in from the edge of the screen — the canonical shadcn/ui sheet built on Radix Dialog, with a side prop, a built-in close X, and a header/footer layout. Official ships it with one demo; the examples below cover what you actually build with it — mobile navigation, forms, custom widths, scrollable content, and a shopping-cart drawer.
Sheet vs Dialog vs Drawer
All three are overlays, and the choice is about placement and intent:
- Sheet anchors to an edge and slides in. Reach for it when the content sits beside the main view — navigation, filters, a settings panel, a cart. It's built on Radix Dialog, so it has the same
open/onOpenChangeAPI as a Dialog. - Dialog is centered and interrupts for a single focused task — a form, a detail view, a confirmation.
- Drawer (built on vaul) is the touch-native bottom sheet with drag-to-dismiss momentum, best on mobile.
A frequent, good pattern is a Sheet on desktop and a Drawer on mobile for the same content.
Four sides
The side prop on SheetContent controls the edge: "top", "right", "bottom", or "left" (default "right"). Left and right sheets take their width from the className (default sm:max-w-sm) and fill the height; top and bottom sheets span the width and size to their content. The Four sides example shows all four from one trigger row.
Mobile navigation
The single most common use of a sheet is the mobile menu — a hamburger button that opens a left sheet of navigation links. The key detail: wrap each link in SheetClose asChild so tapping a link closes the menu as it navigates. Render the hamburger only below your breakpoint (md:hidden) and keep your normal navbar above it. The Mobile navigation example is the complete pattern.
Width & height
Size the panel with a utility class on SheetContent rather than editing the primitive: className="sm:max-w-md", sm:max-w-lg, or w-[90vw] sm:max-w-3xl for a wide panel. Because the width lives in a className, tailwind-merge cleanly overrides the default sm:max-w-sm. For top/bottom sheets, control the height the same way (h-1/2, h-auto). The Widths example demonstrates four sizes.
Controlled & closing
Open the sheet declaratively with SheetTrigger, or controlled from state via open / onOpenChange. To dismiss it: wrap any button in SheetClose asChild for a plain close, or call setOpen(false) to close it programmatically — for example after a form submits successfully. The With a form example controls the sheet and closes it on submit; the Scrollable and Shopping cart examples keep a sticky header and footer around a flex-1 overflow-y-auto body so long lists scroll without losing the actions.
"My sheet won't animate"
If the sheet snaps open with no slide, the animation utilities are missing. The animate-in, slide-in-from-*, and fade-in-0 classes come from tw-animate-css (or the older tailwindcss-animate), not core Tailwind. Add it to your global stylesheet:
@import "tailwindcss";
@import "tw-animate-css";
Without it the sheet is fully functional — it just appears instantly instead of sliding. This is the same setup that powers the Dialog and Alert Dialog animations.
Accessibility
Radix gives the sheet a focus trap, restores focus to the trigger on close, and closes on Escape and outside-click (unlike an Alert Dialog, which blocks outside-click on purpose). Always include a SheetTitle and a SheetDescription so the panel is announced — if your design hides the title, keep it in the DOM with sr-only rather than removing it, or Radix logs an accessibility warning.
Theming
The sheet reads your design tokens — --background, --border, --muted-foreground — so it follows your theme, including dark mode, with no overrides. Style any region with a className (SheetContent, SheetHeader, SheetFooter) to adjust padding, borders, or width per use.