Skip to content
goodtabs UI
BrowseBrowse components

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

PropTypeDefaultDescription
triggerReactElementThe control that opens the panel.
titlestringThe panel title. Also names the scrolling body.
descriptionstringOne line under the title.
footerReactNodeButtons 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