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

1 Configure registry — once per project
{
  "registries": {
    "@designrevision": "https://registry.designrevision.com/r/{name}.json"
  }
}

Add to your existing components.json. Requires shadcn/ui v2.3+.

2 Install the component

Packages

@radix-ui/react-dialog
utils

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.

Copied!
components/ui/sheet.tsx
"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

sheet
button

Props

No props documented yet.

Copied!
components/ui/sheet-demo-01.tsx
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

sheet
button

Props

No props documented yet.

Copied!
components/ui/sheet-sides-01.tsx
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

lucide-react
sheet
button

Props

No props documented yet.

Copied!
components/ui/sheet-nav-01.tsx
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

sheet
button
input
label

Props

No props documented yet.

Copied!
components/ui/sheet-form-01.tsx
"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&apos;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

sheet
button

Props

No props documented yet.

Copied!
components/ui/sheet-sizes-01.tsx
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

sheet
button

Props

No props documented yet.

Copied!
components/ui/sheet-scroll-01.tsx
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

lucide-react
sheet
button

Props

No props documented yet.

Copied!
components/ui/sheet-cart-01.tsx
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>
  )
}

Frequently asked questions

Yes — it is the canonical shadcn/ui sheet, built on Radix Dialog with a side prop and a built-in close X. Official ships the primitive with a single demo; we add four-sides, mobile navigation, a controlled form, custom widths, a scrollable layout, and a shopping-cart drawer.
A Sheet anchors to an edge of the screen and slides in — use it for navigation, filters, carts, and settings that sit beside the main view. A [Dialog](/components/dialog) is centered and use it for a focused modal task like a form or confirmation. Both are built on Radix Dialog and share the same open / onOpenChange API, so the difference is purely placement and intent.
A Drawer (built on vaul) is the touch-native bottom sheet with drag-to-dismiss momentum, best on mobile. A Sheet slides from any edge without drag physics and is the better fit for desktop side panels and responsive navigation. A common pattern is a Sheet on desktop and a Drawer on mobile for the same content.
Pass the side prop to SheetContent: side="top" | "right" | "bottom" | "left". It defaults to "right". Left/right sheets take their width (default sm:max-w-sm) and fill the height; top/bottom sheets fill the width and size to their content.
Set a width utility class on SheetContent — for example className="sm:max-w-lg" or className="w-[90vw] sm:max-w-3xl". The default is sm:max-w-sm. Because the width lives in a className, tailwind-merge lets you override it without editing the primitive. The Widths example shows four sizes.
The slide and fade animations rely on the animate-in and slide-in-from-* utilities, which come from tw-animate-css (or the older tailwindcss-animate), not core Tailwind. If the sheet snaps open with no transition, add @import "tw-animate-css"; to your global CSS (after @import "tailwindcss";). Without it the sheet still works — it just does not animate.
For a plain button, wrap it in SheetClose with asChild — clicking it closes the sheet. To close it programmatically (after a successful submit or a save), control the open state with open / onOpenChange and call setOpen(false). The With a form example does exactly this.