Skip to content
EMBOSS
Docs menu

Sheet

An edge-anchored tray that slides in from any side and hovers above the page with a live penumbra.

@emboss/sheet

Installation

pnpm dlx shadcn@latest add @emboss/sheet

Usage

example.tsx
import { Button } from "@/components/ui/button";
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet";

export function Example() {
  return (
    <Sheet>
      <SheetTrigger render={<Button>Open</Button>} />
      <SheetContent side="right">
        <SheetTitle>Patch bay</SheetTitle>
        <SheetDescription>Route signals between modules.</SheetDescription>
      </SheetContent>
    </Sheet>
  );
}

API reference

Sheet / SheetTrigger / SheetContent

Root state, trigger, and the edge tray. SheetTitle and SheetDescription label the tray.

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""right"Which edge the tray is anchored to (SheetContent).
open / defaultOpen / onOpenChangeboolean / boolean / (open) => voidControlled and uncontrolled open state.

Keyboard

KeysAction
EscCloses the sheet and returns focus to the trigger.

Source

View source — sheet.tsx
sheet.tsx
"use client";

import { Drawer as BaseDrawer } from "@base-ui/react/drawer";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";

const sheetVariants = cva(
  "fixed z-50 flex flex-col gap-4 bg-surface-2 p-6 text-ink shadow-float-2 transition-[translate,opacity] duration-(--duration-float) ease-(--ease-float)",
  {
    variants: {
      side: {
        right:
          "inset-y-0 right-0 w-80 max-w-[85vw] data-ending-style:translate-x-full data-starting-style:translate-x-full",
        left: "inset-y-0 left-0 w-80 max-w-[85vw] data-ending-style:-translate-x-full data-starting-style:-translate-x-full",
        top: "inset-x-0 top-0 max-h-[85vh] data-ending-style:-translate-y-full data-starting-style:-translate-y-full",
        bottom:
          "inset-x-0 bottom-0 max-h-[85vh] rounded-t-lg data-ending-style:translate-y-full data-starting-style:translate-y-full",
      },
    },
    defaultVariants: { side: "right" },
  },
);

/**
 * An edge-anchored tray that slides in over the page — the page keeps a
 * live shadow under it, so it reads as hovering, not painted on.
 *
 * @example
 * <Sheet>
 *   <SheetTrigger render={<Button>Open patch bay</Button>} />
 *   <SheetContent side="right">
 *     <SheetTitle>Patch bay</SheetTitle>
 *   </SheetContent>
 * </Sheet>
 */
function Sheet(props: React.ComponentProps<typeof BaseDrawer.Root>) {
  return <BaseDrawer.Root {...props} />;
}

function SheetTrigger(props: React.ComponentProps<typeof BaseDrawer.Trigger>) {
  return <BaseDrawer.Trigger data-slot="sheet-trigger" {...props} />;
}

function SheetContent({
  className,
  side,
  children,
  ...props
}: React.ComponentProps<typeof BaseDrawer.Popup> &
  VariantProps<typeof sheetVariants>) {
  return (
    <BaseDrawer.Portal>
      <BaseDrawer.Backdrop
        data-slot="sheet-backdrop"
        className="fixed inset-0 z-50 bg-ink/40 transition-opacity duration-(--duration-float) ease-(--ease-float) data-ending-style:opacity-0 data-starting-style:opacity-0"
      />
      <BaseDrawer.Popup
        data-slot="sheet-content"
        className={cn(sheetVariants({ side }), className)}
        {...props}
      >
        <BaseDrawer.Content className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto">
          {children}
        </BaseDrawer.Content>
        <BaseDrawer.Close
          aria-label="Close"
          data-slot="sheet-close-corner"
          className="absolute top-4 right-4 inline-flex size-7 cursor-pointer items-center justify-center rounded-sm text-ink-faint focus-ring transition-[box-shadow,color] hover:text-ink hover:shadow-flush"
        >
          <X className="size-4" aria-hidden />
        </BaseDrawer.Close>
      </BaseDrawer.Popup>
    </BaseDrawer.Portal>
  );
}

function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="sheet-header"
      className={cn("flex flex-col gap-1.5", className)}
      {...props}
    />
  );
}

function SheetTitle({
  className,
  ...props
}: React.ComponentProps<typeof BaseDrawer.Title>) {
  return (
    <BaseDrawer.Title
      data-slot="sheet-title"
      className={cn("text-lg leading-none font-semibold", className)}
      {...props}
    />
  );
}

function SheetDescription({
  className,
  ...props
}: React.ComponentProps<typeof BaseDrawer.Description>) {
  return (
    <BaseDrawer.Description
      data-slot="sheet-description"
      className={cn("text-sm text-ink-muted", className)}
      {...props}
    />
  );
}

function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="sheet-footer"
      className={cn("mt-auto flex justify-end gap-2", className)}
      {...props}
    />
  );
}

function SheetClose(props: React.ComponentProps<typeof BaseDrawer.Close>) {
  return <BaseDrawer.Close data-slot="sheet-close" {...props} />;
}

export {
  Sheet,
  SheetTrigger,
  SheetContent,
  SheetHeader,
  SheetTitle,
  SheetDescription,
  SheetFooter,
  SheetClose,
};