Sheet Panel

PreviousNext

An animated side panel (drawer) for filters, details, settings and quick edits — with a soft overlay, a spring entrance and sections that reveal in sequence.

"use client";

import { Check, SlidersHorizontal } from "lucide-react";
import { useState } from "react";

import {
  SheetPanel,
  SheetPanelBody,
  SheetPanelClose,
  SheetPanelContent,
  SheetPanelDescription,
  SheetPanelFooter,
  SheetPanelHeader,
  SheetPanelSection,
  SheetPanelTitle,
  SheetPanelTrigger,
} from "@/components/matos-ui/sheet-panel";

const STATUSES = ["Active", "Pending", "Archived", "Draft"];
const SORTS = ["Newest", "Oldest", "Price: high", "Price: low"];

export function SheetPanelDemo() {
  const [statuses, setStatuses] = useState<string[]>(["Active"]);
  const [sort, setSort] = useState("Newest");
  const [inStock, setInStock] = useState(true);

  function toggleStatus(value: string) {
    setStatuses((current) =>
      current.includes(value)
        ? current.filter((item) => item !== value)
        : [...current, value],
    );
  }

  return (
    <div className="flex min-h-52 w-full items-center justify-center">
      <SheetPanel>
        <SheetPanelTrigger className="inline-flex h-9 items-center gap-2 rounded-xl border border-border bg-background px-4 font-medium text-foreground text-sm shadow-xs transition-[background-color,transform] duration-200 hover:bg-muted/60 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
          <SlidersHorizontal className="size-4" aria-hidden="true" />
          Filters
        </SheetPanelTrigger>

        <SheetPanelContent side="right" size="md">
          <SheetPanelHeader>
            <SheetPanelTitle>Filters</SheetPanelTitle>
            <SheetPanelDescription>
              Refine the results. Changes apply when you save.
            </SheetPanelDescription>
          </SheetPanelHeader>

          <SheetPanelBody>
            <SheetPanelSection title="Status">
              <div className="flex flex-wrap gap-2">
                {STATUSES.map((status) => {
                  const active = statuses.includes(status);
                  return (
                    <button
                      key={status}
                      type="button"
                      onClick={() => toggleStatus(status)}
                      aria-pressed={active}
                      className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 font-medium text-xs transition-[background-color,border-color,color,transform] duration-200 active:scale-[0.97] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
                        active
                          ? "border-primary bg-primary text-primary-foreground"
                          : "border-border bg-background text-muted-foreground hover:border-border hover:bg-muted/60 hover:text-foreground"
                      }`}
                    >
                      {active ? (
                        <Check className="size-3" aria-hidden="true" />
                      ) : null}
                      {status}
                    </button>
                  );
                })}
              </div>
            </SheetPanelSection>

            <SheetPanelSection title="Sort by">
              <div className="grid grid-cols-2 gap-2">
                {SORTS.map((option) => {
                  const active = sort === option;
                  return (
                    <button
                      key={option}
                      type="button"
                      onClick={() => setSort(option)}
                      aria-pressed={active}
                      className={`rounded-xl border px-3 py-2 text-left font-medium text-sm transition-[background-color,border-color,transform] duration-200 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
                        active
                          ? "border-ring bg-muted/60 text-foreground ring-2 ring-ring/25"
                          : "border-border bg-background text-muted-foreground hover:bg-muted/40 hover:text-foreground"
                      }`}
                    >
                      {option}
                    </button>
                  );
                })}
              </div>
            </SheetPanelSection>

            <SheetPanelSection title="Availability">
              <button
                type="button"
                onClick={() => setInStock((current) => !current)}
                aria-pressed={inStock}
                className="flex items-center justify-between rounded-xl border border-border bg-background px-4 py-3 text-left transition-colors duration-200 hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
              >
                <span className="flex flex-col">
                  <span className="font-medium text-foreground text-sm">
                    In stock only
                  </span>
                  <span className="text-muted-foreground text-xs">
                    Hide sold-out items
                  </span>
                </span>
                <span
                  className={`relative h-6 w-10 shrink-0 rounded-full transition-colors duration-300 ${
                    inStock ? "bg-primary" : "bg-muted"
                  }`}
                >
                  <span
                    className={`absolute top-0.5 size-5 rounded-full bg-background shadow-sm transition-transform duration-300 ease-[cubic-bezier(0.32,1.36,0.5,1)] ${
                      inStock ? "translate-x-[1.125rem]" : "translate-x-0.5"
                    }`}
                  />
                </span>
              </button>
            </SheetPanelSection>
          </SheetPanelBody>

          <SheetPanelFooter>
            <SheetPanelClose className="inline-flex h-9 items-center justify-center rounded-xl border border-border bg-background px-4 font-medium text-foreground text-sm transition-colors duration-200 hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
              Reset
            </SheetPanelClose>
            <SheetPanelClose className="inline-flex h-9 items-center justify-center rounded-xl bg-primary px-4 font-medium text-primary-foreground text-sm shadow-xs transition-[background-color,transform] duration-200 hover:bg-primary/90 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
              Apply filters
            </SheetPanelClose>
          </SheetPanelFooter>
        </SheetPanelContent>
      </SheetPanel>
    </div>
  );
}

Installation

pnpm dlx shadcn@latest add https://matos-ui.com/r/sheet-panel.json

Usage

import {
  SheetPanel,
  SheetPanelTrigger,
  SheetPanelContent,
  SheetPanelHeader,
  SheetPanelTitle,
  SheetPanelDescription,
  SheetPanelBody,
  SheetPanelSection,
  SheetPanelFooter,
  SheetPanelClose
} from '@/components/matos-ui/sheet-panel'
<SheetPanel>
  <SheetPanelTrigger>Open</SheetPanelTrigger>
  <SheetPanelContent side="right" size="md">
    <SheetPanelHeader>
      <SheetPanelTitle>Filters</SheetPanelTitle>
      <SheetPanelDescription>Refine the results.</SheetPanelDescription>
    </SheetPanelHeader>
    <SheetPanelBody>
      <SheetPanelSection title="Status">{/* ... */}</SheetPanelSection>
    </SheetPanelBody>
    <SheetPanelFooter>
      <SheetPanelClose>Cancel</SheetPanelClose>
      <SheetPanelClose>Apply</SheetPanelClose>
    </SheetPanelFooter>
  </SheetPanelContent>
</SheetPanel>

Built on Base UI's Dialog, so focus trapping, scroll locking, the escape key and outside-press dismissal all work out of the box.

When to use

  • Filters — let users refine a list without leaving the page.
  • Details — show the full record for a selected row.
  • Settings — group configuration away from the main view.
  • Quick edit — edit an item inline and save.

For short confirmations or focused tasks, prefer a centered dialog.

Examples

Sides

<SheetPanelContent side="left" />
<SheetPanelContent side="right" />
<SheetPanelContent side="top" />
<SheetPanelContent side="bottom" />

Sizes

size maps to a max width for left/right panels, and a max height for top/bottom panels.

<SheetPanelContent side="right" size="lg" />

Controlled

const [open, setOpen] = useState(false)

<SheetPanel open={open} onOpenChange={setOpen}>
  <SheetPanelContent>...</SheetPanelContent>
</SheetPanel>

Micro-interactions

  • Soft overlay — the backdrop fades in and blurs the page behind it.
  • Spring entrance — the panel slides in with a gentle overshoot easing, and slides out cleanly on close.
  • Sequenced sectionsHeader, Body sections and Footer rise and de-blur one after another via Framer Motion.
  • Reduced motion — all motion is disabled when the user prefers reduced motion.

Reference

SheetPanel

Wraps Base UI Dialog.Root. Accepts open, defaultOpen, onOpenChange and modal.

SheetPanelContent

PropTypeDefaultDescription
side"right" | "left" | "top" | "bottom""right"Edge the panel slides in from.
size"sm" | "md" | "lg" | "xl""md"Max width (sides) or max height (top/bottom).
showClosebooleantrueRender the built-in close button.
backdropClassNamestringClass override for the backdrop.
contentClassNamestringClass override for the inner content wrapper.

Subcomponents

ComponentDescription
SheetPanelTriggerOpens the panel (renders a button).
SheetPanelCloseCloses the panel (renders a button).
SheetPanelHeaderSequenced header region.
SheetPanelTitleAccessible title (Dialog.Title).
SheetPanelDescriptionAccessible description (Dialog.Description).
SheetPanelBodyScrollable region that sequences its own sections.
SheetPanelSectionTitled, sequenced group of controls.
SheetPanelItemGeneric sequenced block.
SheetPanelFooterSequenced footer with a top divider for actions.

Also exported: sheetPanelPopupVariants and sheetPanelBackdropVariants.

Best practices

  • Keep one primary action in the footer; make secondary actions less prominent.
  • Use side="bottom" on mobile for thumb-friendly reach.
  • Always include a SheetPanelTitle for screen readers, even if visually subtle.
  • Persist draft state and only commit on save for filters and quick edits.