Sheet panel
A side panel with header, scrolling body and pinned footer slots.
npx shadcn@latest add https://ui.goodtabs.co/r/sheet-panel.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/sheet-panel
Right side panel with actions pinned to the bottom.
Left, no footer
Slides from the left, long body scrolls.
Usage
import { SheetPanel } from "@/components/ui/sheet-panel"
import { Button } from "@/components/ui/button"
<SheetPanel trigger={<Button variant="outline">Filters</Button>} title="Filters" footer={<Button>Apply</Button>}>
...
</SheetPanel>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| trigger | ReactElement | The control that opens the panel. | |
| title | string | The panel title. Also names the scrolling body. | |
| description | string | One line under the title. | |
| footer | ReactNode | Buttons for the bottom edge. Wrap in SheetClose to dismiss. | |
| side | "right" | "left" | "right" | Which edge the panel slides from. |
Source
sheet-panel/sheet-panel.tsx
import * as React from "react";
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
type SheetPanelProps = {
/** The control that opens the panel. Passed to SheetTrigger as the child element. */
trigger: React.ReactElement;
/** The panel title. Also names the scrolling body for assistive tech. */
title: string;
/** One line under the title. */
description?: string;
/** Buttons for the bottom edge. Wrap a button in SheetClose to dismiss on click. */
footer?: React.ReactNode;
/** Which edge the panel slides from. */
side?: "right" | "left";
/** The panel body. It scrolls when it is taller than the window. */
children: React.ReactNode;
};
/**
* A side panel with three slots: header, a scrolling body and a pinned footer.
* Built on the shadcn sheet, so focus trap, Escape and the overlay come with it.
*/
export function SheetPanel({ trigger, title, description, footer, side = "right", children }: SheetPanelProps) {
return (
<Sheet>
<SheetTrigger asChild>{trigger}</SheetTrigger>
<SheetContent side={side} className="gap-0 p-0 sm:max-w-md">
<SheetHeader className="border-b p-4 pr-12">
<SheetTitle>{title}</SheetTitle>
{description ? (
<SheetDescription>{description}</SheetDescription>
) : (
<SheetDescription className="sr-only">{title}</SheetDescription>
)}
</SheetHeader>
<div
role="region"
aria-label={title}
tabIndex={0}
className="flex-1 overflow-y-auto p-4 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
>
{children}
</div>
{footer && <SheetFooter className="mt-0 flex-row justify-end border-t">{footer}</SheetFooter>}
</SheetContent>
</Sheet>
);
}
For AI agents
Hand your agent this page, the markdown at /components/sheet-panel/prompt.md, or the Copy AI prompt button above.
When to use
- Filters, details or settings that sit beside the page
- Secondary tasks that should not leave the current screen
- Content longer than a dialog should hold
When not to use
- A short yes or no, which wants a confirm dialog
- Mobile first menus, which want the bottom drawer
- Main navigation
Agent notes
- Thin wrapper over @/components/ui/sheet; focus trap, Escape and overlay come from radix
- The body is a focusable scrolling region labelled with the title
- Footer is optional; omit it for read only panels
- Uncontrolled; for controlled state use Sheet from @/components/ui/sheet directly