Shadcn Sheet for React and Tailwind
Content that slides in from the screen edge.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/sheet.jsonpnpm dlx shadcn@latest add https://kit.dev/r/sheet.jsonnpx shadcn@latest add https://kit.dev/r/sheet.jsonyarn shadcn@latest add https://kit.dev/r/sheet.json<Step>This component depends on Button and Dialog. Install them first if you haven't already.</Step>
Install the following dependencies:
bun add @ark-ui/react lucide-react tailwind-variantspnpm add @ark-ui/react lucide-react tailwind-variantsnpm install @ark-ui/react lucide-react tailwind-variantsyarn add @ark-ui/react lucide-react tailwind-variantsCopy and paste the following code into your project.
"use client";
import { Dialog as ArkDialog, useDialogContext } from "@ark-ui/react/dialog";
import { Portal } from "@ark-ui/react/portal";
import { XIcon } from "lucide-react";
import type React from "react";
import { tv, type VariantProps } from "tailwind-variants";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogBody,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogTitle,
} from "@/components/ui/dialog";
export const useSheet = useDialogContext;
export const Sheet = (props: React.ComponentProps<typeof Dialog>) => (
<Dialog data-slot="sheet" {...props} />
);
export const SheetTrigger = (
props: React.ComponentProps<typeof ArkDialog.Trigger>
) => <ArkDialog.Trigger data-slot="sheet-trigger" {...props} />;
export const SheetOverlay = (
props: React.ComponentProps<typeof DialogOverlay>
) => <DialogOverlay data-slot="sheet-overlay" {...props} />;
const sheetPositionerVariants = tv({
base: [
"[--inset:--spacing(0)]",
"fixed inset-0 z-50",
"h-svh w-screen",
"grid",
"overflow-hidden",
],
variants: {
placement: {
bottom: "grid grid-rows-[1fr_auto] not-data-[variant=inset]:pt-12",
top: "grid grid-rows-[auto_1fr] not-data-[variant=inset]:pb-12",
left: "flex justify-start",
right: "flex justify-end",
},
variant: {
default: "",
inset: [
"px-(--inset) sm:[--inset:--spacing(4)]",
"data-[placement=bottom]:pb-(--inset)",
"data-[placement=top]:pt-(--inset)",
"data-[placement=left]:pt-(--inset) data-[placement=left]:pb-(--inset)",
"data-[placement=right]:pt-(--inset) data-[placement=right]:pb-(--inset)",
],
},
},
defaultVariants: {
variant: "default",
},
});
interface SheetPositionerProps
extends React.ComponentProps<typeof ArkDialog.Positioner>,
VariantProps<typeof sheetPositionerVariants> {}
export const SheetPositioner = (props: SheetPositionerProps) => {
const { variant = "default", placement, className, ...rest } = props;
return (
<ArkDialog.Positioner
className={cn(sheetPositionerVariants({ placement, variant }), className)}
data-placement={placement}
data-slot="sheet-positioner"
data-variant={variant}
{...rest}
/>
);
};
const sheetContentVariants = tv({
base: [
"[--space:--spacing(6)]",
"relative",
"max-h-full min-h-0 w-full min-w-0",
"flex flex-col",
"bg-popover",
"text-popover-foreground",
"shadow-lg/5",
"transition-[opacity,translate] duration-200 ease-in-out will-change-transform",
"data-[state=closed]:fade-out-0 data-[state=closed]:animate-out",
"data-[state=open]:fade-in-0 data-[state=open]:animate-in",
"motion-reduce:animate-none! motion-reduce:transition-none!",
],
variants: {
placement: {
bottom: [
"row-start-2 border-t",
"data-[state=closed]:slide-in-from-bottom-10 data-[state=open]:slide-in-from-bottom-10",
],
top: [
"border-b",
"data-[state=closed]:slide-out-to-top-10 data-[state=open]:slide-in-from-top-10",
],
left: [
"w-[calc(100%-(--spacing(12)))] max-w-md",
"col-start-2",
"border-e",
"data-[state=closed]:slide-out-to-start-10 data-[state=open]:slide-in-from-start-10",
],
right: [
"w-[calc(100%-(--spacing(12)))] max-w-md",
"col-start-2",
"border-s",
"data-[state=closed]:slide-out-to-end-10 data-[state=open]:slide-in-from-end-10",
],
},
variant: {
default: "",
inset: [
"sm:rounded-2xl sm:border",
"sm:**:data-[slot=sheet-footer]:rounded-b-[calc(var(--radius-2xl)-1px)]",
],
},
},
defaultVariants: {
placement: "right",
variant: "default",
},
});
interface SheetContentProps
extends React.ComponentProps<typeof ArkDialog.Content>,
VariantProps<typeof sheetContentVariants> {
/**
* Show close button at the top right corner
*
* @default true
*/
showCloseButton?: boolean;
}
export const SheetContent = (props: SheetContentProps) => {
const {
showCloseButton = true,
placement = "right",
variant = "default",
className,
children,
...rest
} = props;
return (
<Portal>
<SheetOverlay />
<SheetPositioner placement={placement} variant={variant}>
<ArkDialog.Content
className={cn(
sheetContentVariants({ placement, variant }),
className
)}
data-slot="sheet-content"
{...rest}
>
{children}
{!!showCloseButton && (
<SheetClose asChild>
<Button
aria-label="Close"
className="absolute inset-e-2 top-2 opacity-64 hover:opacity-100"
size="icon-sm"
variant="ghost"
>
<XIcon />
</Button>
</SheetClose>
)}
</ArkDialog.Content>
</SheetPositioner>
</Portal>
);
};
export const SheetHeader = (
props: React.ComponentProps<typeof DialogHeader>
) => <DialogHeader data-slot="sheet-header" {...props} />;
export const SheetTitle = (props: React.ComponentProps<typeof DialogTitle>) => (
<DialogTitle data-slot="sheet-title" {...props} />
);
export const SheetDescription = (
props: React.ComponentProps<typeof DialogDescription>
) => <DialogDescription data-slot="sheet-description" {...props} />;
export const SheetBody = (props: React.ComponentProps<typeof DialogBody>) => {
const { className, ...rest } = props;
return (
<DialogBody
className={cn(
"in-[[data-slot=sheet-content]:has([data-slot=sheet-header])]:pt-0",
className
)}
data-slot="sheet-body"
{...rest}
/>
);
};
export const SheetClose = (
props: React.ComponentProps<typeof ArkDialog.CloseTrigger>
) => <ArkDialog.CloseTrigger data-slot="sheet-close" {...props} />;
export const SheetFooter = (
props: React.ComponentProps<typeof DialogFooter>
) => {
const { className, ...rest } = props;
return (
<DialogFooter
className={cn("sm:rounded-none", className)}
data-slot="sheet-footer"
{...rest}
/>
);
};Update the import paths to match your project setup.
Anatomy
Sheet
├── SheetTrigger
└── SheetContent
├── SheetHeader
│ ├── SheetTitle
│ └── SheetDescription
├── SheetBody
└── SheetFooter
└── SheetCloseUsage
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger
} from "@/components/ui/sheet";<Sheet>
<SheetTrigger>Open</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Sheet Title</SheetTitle>
<SheetDescription>Sheet Description</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>Controlled
Use open and onOpenChange on the root to control the sheet state.
Sides
Control which side the sheet slides in from using the side prop.
Examples
Inset Variant
Use the variant="inset" prop to add padding. The sheet appears as a floating card.
Scrollable
Use SheetBody to make the content area scrollable while keeping header and footer fixed.
No Close Button
Use the showCloseButton={false} prop to hide the close button in the top right corner.
Close behavior
Use closeOnInteractOutside and closeOnEscape props to prevent closing on outside click and escape.
Non-Modal
Use modal={false} to allow interaction with elements outside the sheet.
Custom spacing
Use [--space:--spacing("value")] on SheetContent to adjust internal padding.
Default spacing is --spacing(6).
You can use breakpoint utilities to change the internal spacing at different screen sizes.
md:[--space:--spacing(6)] lg:[--space:--spacing(8)]API Reference
Sheet
Root component. Uses Ark UI Dialog. Controls open state and slide direction.
| Prop | Type | Default |
|---|---|---|
open | boolean | - |
defaultOpen | boolean | false |
onOpenChange | ({ open }: OpenChangeDetails) => void | - |
modal | boolean | true |
closeOnEscape | boolean | true |
closeOnInteractOutside | boolean | true |
initialFocusEl | () => MaybeElement | - |
finalFocusEl | () => MaybeElement | - |
onEscapeKeyDown | (event: KeyboardEvent) => void | - |
onInteractOutside | (event: InteractOutsideEvent) => void | - |
className | string | - |
SheetTrigger
Opens the sheet on click. Use asChild for custom triggers.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
className | string | - |
SheetContent
Holds the sheet panel. Slides in from the configured edge.
| Prop | Type | Default |
|---|---|---|
side | "right" | "left" | "top" | "bottom" | "right" |
variant | "default" | "inset" | "default" |
showCloseButton | boolean | true |
className | string | - |
| Attribute | Default |
|---|---|
--space | --spacing(6) |
SheetHeader
Header area for title and description.
| Prop | Type | Default |
|---|---|---|
title | string | - |
description | string | - |
className | string | - |
SheetBody
Scrollable body content. Uses ScrollArea for overflow.
| Prop | Type | Default |
|---|---|---|
scrollFade | boolean | false |
className | string | - |
SheetFooter
Footer area for action buttons.
| Prop | Type | Default |
|---|---|---|
className | string | - |
SheetTitle
Accessible title. Announced to screen readers.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
className | string | - |
SheetDescription
Accessible description. Announced to screen readers.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
className | string | - |
SheetClose
Closes the sheet on click.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
className | string | - |
SheetOverlay
Dimmed overlay behind the sheet.
| Prop | Type | Default |
|---|---|---|
className | string | - |
SheetPositioner
Positioning wrapper for sheet content. Used internally; expose for custom layouts.
| Prop | Type | Default |
|---|---|---|
side | "right" | "left" | "top" | "bottom" | - |
variant | "default" | "inset" | "default" |
className | string | - |
For a complete list of props, see the Ark UI documentation.