import { Button } from "@/components/ui/button";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
Sheet,
SheetBody,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Textarea } from "@/components/ui/textarea";
export default function SheetBasic() {
return (
<Sheet>
<SheetTrigger render={<Button variant="secondary" />}>Edit task</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Update pricing page</SheetTitle>
<SheetDescription>Website redesign · Task 14</SheetDescription>
</SheetHeader>
<SheetBody>
<FieldGroup className="gap-3">
<Field>
<FieldLabel htmlFor="task-title">Title</FieldLabel>
<Input id="task-title" defaultValue="Update pricing page" />
</Field>
<Field>
<FieldLabel htmlFor="task-notes">Notes</FieldLabel>
<Textarea
id="task-notes"
defaultValue="Match the new plan names and add the comparison table."
/>
</Field>
</FieldGroup>
</SheetBody>
<SheetFooter>
<SheetClose render={<Button variant="secondary" />}>Cancel</SheetClose>
<SheetClose render={<Button variant="primary" />}>Save</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
);
}- Add the registryOnce per project
pnpm dlx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json - Add sheet
pnpm dlx shadcn@latest add @jarvis/sheet
Usage
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetBody,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
export function FiltersSheet() {
return (
<Sheet>
<SheetTrigger render={<Button variant="secondary" />}>Filters</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Filters</SheetTitle>
<SheetDescription>Narrow down the task list.</SheetDescription>
</SheetHeader>
<SheetBody>…</SheetBody>
<SheetFooter>
<SheetClose render={<Button variant="primary" />}>Apply</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
);
}A sheet is a Dialog shaped as a side panel. Always include a SheetTitle.
Composition
Examples
Scrolling body
SheetBody scrolls between a fixed header and footer.
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Kbd } from "@/components/ui/kbd";
import {
Sheet,
SheetBody,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
const activity = [
{ who: "Sam Rivera", initials: "SR", what: "moved this task to In progress", when: "2h ago" },
{ who: "Ada Park", initials: "AP", what: "attached homepage-v2.fig", when: "5h ago" },
{
who: "Priya Shah",
initials: "PS",
what: "commented: “Let’s ship the mobile layout first.”",
when: "Yesterday",
},
{ who: "Jonah Kim", initials: "JK", what: "changed the due date to Mar 28", when: "Yesterday" },
{ who: "Sam Rivera", initials: "SR", what: "added the Pricing label", when: "Mar 14" },
{
who: "Lena Ortiz",
initials: "LO",
what: "linked this task to the launch checklist",
when: "Mar 13",
},
{ who: "Marco Ruiz", initials: "MR", what: "assigned this task to Sam Rivera", when: "Mar 12" },
{ who: "Ada Park", initials: "AP", what: "renamed this task", when: "Mar 12" },
{ who: "Priya Shah", initials: "PS", what: "added the Design label", when: "Mar 11" },
{ who: "Jonah Kim", initials: "JK", what: "commented: “Copy is final.”", when: "Mar 11" },
{ who: "Lena Ortiz", initials: "LO", what: "attached pricing-copy.docx", when: "Mar 10" },
{ who: "Sam Rivera", initials: "SR", what: "set the priority to High", when: "Mar 9" },
{ who: "Marco Ruiz", initials: "MR", what: "moved this task to Up next", when: "Mar 8" },
{ who: "Ada Park", initials: "AP", what: "added a checklist with 4 items", when: "Mar 8" },
{ who: "Priya Shah", initials: "PS", what: "set the due date to Mar 21", when: "Mar 7" },
{ who: "Jonah Kim", initials: "JK", what: "added Lena Ortiz as a watcher", when: "Mar 6" },
{ who: "Lena Ortiz", initials: "LO", what: "linked the brand guidelines", when: "Mar 5" },
{ who: "Sam Rivera", initials: "SR", what: "added the Website label", when: "Mar 4" },
{ who: "Ada Park", initials: "AP", what: "created this task", when: "Mar 3" },
];
export default function SheetScrolling() {
return (
<Sheet>
<SheetTrigger render={<Button variant="secondary" />}>View activity</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Activity</SheetTitle>
<SheetDescription>Update pricing page · Task 14</SheetDescription>
</SheetHeader>
<SheetBody className="gap-3">
{activity.map((entry, index) => (
<div key={index} className="flex gap-3 text-sm">
<Avatar size="sm" className="mt-0.5">
<AvatarFallback>{entry.initials}</AvatarFallback>
</Avatar>
<p className="min-w-0 flex-1 text-muted-foreground">
<span className="font-medium text-foreground">{entry.who}</span> {entry.what}
<span className="block text-xs text-faint">{entry.when}</span>
</p>
</div>
))}
</SheetBody>
<SheetFooter>
<span className="mr-auto flex items-center gap-2 text-2xs font-medium text-muted-foreground">
<Kbd size="sm">Esc</Kbd>
Close
</span>
<SheetClose render={<Button variant="secondary" />}>Done</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
);
}Sides
side sets the edge the sheet slides in from.
import { LinkIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Sheet,
SheetBody,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
const sides = ["top", "right", "bottom", "left"] as const;
export default function SheetSides() {
return (
<div className="flex flex-wrap items-center gap-3">
{sides.map((side) => (
<Sheet key={side}>
<SheetTrigger render={<Button variant="secondary" className="capitalize" />}>
{side}
</SheetTrigger>
<SheetContent side={side}>
<SheetHeader>
<SheetTitle>Share “Mobile app”</SheetTitle>
<SheetDescription>Invite people or copy a link.</SheetDescription>
</SheetHeader>
<SheetBody className="gap-3 sm:flex-row">
<Input
placeholder="name@example.com"
aria-label="Email address"
className="sm:max-w-sm"
/>
<Button variant="secondary">
<LinkIcon />
Copy link
</Button>
</SheetBody>
</SheetContent>
</Sheet>
))}
</div>
);
}Props
| Part | Extends |
|---|---|
| Sheet | Base UI's Dialog.Root |
| SheetTrigger | Base UI's Dialog.Trigger |
| SheetClose | Base UI's Dialog.Close |
| SheetContent | Base UI's Dialog.Popup |
| SheetHeader | <div> |
| SheetBody | <div> |
| SheetFooter | <div> |
| SheetTitle | Base UI's Dialog.Title |
| SheetDescription | Base UI's Dialog.Description |
The tables leave out className, style and render. Parts built on Base UI take them as its styling and composition guides describe.
Sheet
| Prop | Type | Default |
|---|---|---|
openWhether the dialog is currently open. | boolean | — |
defaultOpenWhether the dialog is initially open. To render a controlled dialog, use the | boolean | false |
modalDetermines if the dialog enters a modal state when open.
When | "trap-focus" | boolean | true |
onOpenChangeEvent handler called when the dialog is opened or closed. | ((open: boolean, eventDetails: Sheet.Root.ChangeEventDetails) => void) | — |
onOpenChangeCompleteEvent handler called after any animations complete when the dialog is opened or closed. | ((open: boolean) => void) | — |
disablePointerDismissalWhether to prevent the dialog from closing on outside presses. For non-modal dialogs, this also prevents the dialog from closing when focus moves outside of it. | boolean | false |
actionsRefA ref to imperative actions.
| React.RefObject<DialogRootActions | null> | — |
handleA handle to associate the dialog with a trigger. If specified, allows external triggers to control the dialog's open state. Can be created with the Dialog.createHandle() method. | Sheet.Handle<unknown> | — |
childrenThe content of the dialog.
This can be a regular React node or a render function that receives the | PayloadChildRenderFunction<unknown> | React.ReactNode | — |
triggerIdID of the trigger that the dialog is associated with.
This is useful in conjunction with the | string | null | — |
defaultTriggerIdID of the trigger that the dialog is associated with.
This is useful in conjunction with the | string | null | — |
SheetTrigger
| Prop | Type | Default |
|---|---|---|
handleA handle to associate the trigger with a dialog. Can be created with the Dialog.createHandle() method. | Sheet.Handle<unknown> | — |
payloadA payload to pass to the dialog when it is opened. | unknown | — |
idID of the trigger. In addition to being forwarded to the rendered element,
it is also used to specify the active trigger for the dialog in controlled mode (with the DialogRoot | string | — |
nativeButtonWhether the component renders a native | boolean | true |
SheetClose
| Prop | Type | Default |
|---|---|---|
nativeButtonWhether the component renders a native | boolean | true |
SheetContent
| Prop | Type | Default |
|---|---|---|
initialFocusDetermines the element to focus when the dialog is opened. By default, focus moves to the first tabbable element inside the popup, except when the dialog is opened by touch — then the popup itself is focused to avoid opening the virtual keyboard.
| boolean | React.RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | HTMLElement | null | void) | — |
finalFocusDetermines the element to focus when the dialog is closed.
| boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | HTMLElement | null | void) | — |
side | "bottom" | "left" | "right" | "top" | "right" |
showCloseButton | boolean | true |