Componentfeedback
Sheet
Slide-over drawer panel with focus trap and scroll lock.
Preview
Live Preview
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add sheetOr install the full npm package:
npm install bigbulluiUsage
import { Sheet, SheetHeader, SheetTitle, SheetDescription } from "@/components/ui/sheet";
<Sheet open={open} onOpenChange={setOpen} side="right">
<SheetHeader>
<SheetTitle>Ticket Drawer</SheetTitle>
<SheetDescription>Order overview.</SheetDescription>
</SheetHeader>
</Sheet>Source code
sheet.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
const FOCUSABLE =
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';
export interface SheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
side?: "right" | "left";
children: React.ReactNode;
}
export function Sheet({
open,
onOpenChange,
side = "right",
children,
}: SheetProps) {
const contentRef = React.useRef<HTMLDivElement>(null);
const restoreRef = React.useRef<HTMLElement | null>(null);
React.useEffect(() => {
if (!open) return;
restoreRef.current = document.activeElement as HTMLElement | null;
const content = contentRef.current;
const focusables = content?.querySelectorAll<HTMLElement>(FOCUSABLE);
(focusables?.[0] ?? content)?.focus();
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
event.preventDefault();
onOpenChange(false);
return;
}
if (event.key !== "Tab" || !content) return;
const items = Array.from(content.querySelectorAll<HTMLElement>(FOCUSABLE));
if (items.length === 0) return;
const first = items[0]!;
const last = items[items.length - 1]!;
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
};
document.addEventListener("keydown", onKeyDown);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKeyDown);
document.body.style.overflow = "";
restoreRef.current?.focus();
};
}, [open, onOpenChange]);
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex">
<div
aria-hidden="true"
onClick={() => onOpenChange(false)}
className="fixed inset-0 bg-black/50 backdrop-blur-[2px] animate-[fade-in_0.2s_ease-out_both]"
/>
<div
ref={contentRef}
role="dialog"
aria-modal="true"
tabIndex={-1}
className={cn(
"fixed inset-y-0 z-50 flex w-full max-w-md flex-col border-[1.5px] border-foreground bg-card p-6 shadow-xl outline-1 outline-dashed outline-offset-[-7px] transition-transform duration-200 ease-out focus:outline-none",
side === "right"
? "right-0 animate-[fade-in-up_0.25s_ease-out]"
: "left-0 animate-[fade-in_0.25s_ease-out]"
)}
>
{children}
<button
type="button"
aria-label="Close"
onClick={() => onOpenChange(false)}
className="absolute right-4 top-4 cursor-pointer rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>
</div>
);
}
export function SheetHeader({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("mb-4 flex flex-col gap-1.5 text-left", className)} {...props} />;
}
export function SheetTitle({
className,
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return <h2 className={cn("text-lg font-semibold tracking-tight", className)} {...props} />;
}
export function SheetDescription({
className,
...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
return <p className={cn("text-sm text-muted-foreground", className)} {...props} />;
}
Props
| Prop | Type | Description |
|---|---|---|
| open | boolean | Controlled open state (required). |
| onOpenChange | (open: boolean) => void | Open state change callback (required). |
| side | "right" | "left" | Entrance side (default 'right'). |