Sheet
A panel that slides in from an edge of the screen over a scrim.
Vibra casts --shadow-modal over the theme's --overlay scrim instead of shadow-lg over 10% black, and slides the panel on the emphasized ease over --duration-slow; the title is set in the display register at text-xl — a different register given to it (type-label, type-eyebrow) replaces that one whole, face, size, tracking and ink, rather than sharing the element with it — and below md the close button grows to a 40px thumb target. The close button sits at the inline end, where shadcn's sits at a physical right, over the start of a right-to-left title, and closeLabel names it in the sheet's own language. It is Base UI's Dialog: titled and described, it opens with the focus on its first control, keeps it inside, and hands it back to its trigger — or to finalFocus — when it closes.
Install
npx shadcn@latest add @vibra/sheetNeeds the @vibra registry in your components.json — set it up once.
Examples
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
export default function SheetDemo() {
return (
<Sheet>
<SheetTrigger render={<Button variant="outline" />}>Edit customer</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Harbour Logistics</SheetTitle>
<SheetDescription>Changes apply to every invoice sent from now on.</SheetDescription>
</SheetHeader>
<div className="flex flex-col gap-4 px-4">
<div className="flex flex-col gap-2">
<Label htmlFor="sheet-billing-name">Billing name</Label>
<Input id="sheet-billing-name" defaultValue="Harbour Logistics Ltd." />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="sheet-billing-email">Billing email</Label>
<Input id="sheet-billing-email" type="email" defaultValue="accounts@harbour.example" />
</div>
</div>
<SheetFooter>
<SheetClose render={<Button />}>Save changes</SheetClose>
<SheetClose render={<Button variant="outline" />}>Cancel</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
)
}Filters
An orders table's filters in a sheet, so the table keeps its width: the foot counts what they would show before they apply, and the trigger says how many are on.
"use client"
import * as React from "react"
import { SlidersHorizontalIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
const STATUSES = [
{ value: "paid", label: "Paid", count: 184 },
{ value: "pending", label: "Pending", count: 41 },
{ value: "overdue", label: "Overdue", count: 12 },
{ value: "refunded", label: "Refunded", count: 7 },
]
// The orders table's filters, in a sheet so the table keeps its width. The
// button at the foot counts what the filters would show before they apply,
// so nobody applies their way into an empty table; the trigger says how
// many filters are on. The sheet opens with the focus on its first box.
export default function SheetFilters() {
const [draft, setDraft] = React.useState(["pending", "overdue"])
const [applied, setApplied] = React.useState(draft)
// No status ticked is no status filter: every order shows.
const shown = STATUSES.filter((status) => !draft.length || draft.includes(status.value)).reduce((sum, status) => sum + status.count, 0)
return (
<Sheet onOpenChange={(open) => open && setDraft(applied)}>
<SheetTrigger render={<Button variant="outline" />}>
<SlidersHorizontalIcon aria-hidden="true" data-icon="inline-start" />
Filters{" "}
<span className="text-muted-foreground tabular-nums">{applied.length}</span>{" "}
<span className="sr-only">on</span>
</SheetTrigger>
<SheetContent className="w-80 sm:max-w-80">
<SheetHeader>
<SheetTitle>Filter orders</SheetTitle>
<SheetDescription>Only orders that match every filter show.</SheetDescription>
</SheetHeader>
<fieldset className="flex flex-col gap-3 px-4">
<legend className="mb-3 type-eyebrow">Status</legend>
{STATUSES.map((status) => (
<div key={status.value} className="flex items-center gap-2">
<Checkbox
id={`sheet-filters-${status.value}`}
checked={draft.includes(status.value)}
onCheckedChange={(checked) =>
setDraft((current) => (checked ? [...current, status.value] : current.filter((value) => value !== status.value)))
}
/>
<Label htmlFor={`sheet-filters-${status.value}`} className="flex-1 font-normal">
{status.label}
</Label>
<span className="text-xs text-muted-foreground tabular-nums">{status.count}</span>
</div>
))}
</fieldset>
<SheetFooter className="flex-row justify-between border-t border-rule">
<Button variant="ghost" disabled={draft.length === 0} focusableWhenDisabled onClick={() => setDraft([])}>
Clear all
</Button>
<SheetClose render={<Button />} onClick={() => setApplied(draft)}>
Show {shown} orders
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
)
}Navigation from the left
The app's navigation on a phone, from the edge where the sidebar sits on a wide screen; the current page is marked on the brand tint and with aria-current.
import { ChartColumnIcon, LayoutDashboardIcon, MenuIcon, ReceiptTextIcon, SettingsIcon, UsersIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
const PAGES = [
{ href: "/overview", label: "Overview", icon: LayoutDashboardIcon },
{ href: "/reports", label: "Reports", icon: ChartColumnIcon },
{ href: "/customers", label: "Customers", icon: UsersIcon },
{ href: "/billing/invoices", label: "Invoices", icon: ReceiptTextIcon },
{ href: "/settings", label: "Settings", icon: SettingsIcon },
]
// The app's navigation on a phone, from the left edge where the sidebar sits
// on a wide screen. It is a titled dialog, so a screen reader hears where it
// is; the current page is marked on the brand tint and with aria-current.
export default function SheetNav({ current = "/customers" }: { current?: string }) {
return (
<Sheet>
<SheetTrigger render={<Button variant="outline" size="icon" aria-label="Open navigation" />}>
<MenuIcon aria-hidden="true" />
</SheetTrigger>
<SheetContent side="left" className="w-72 gap-2 sm:max-w-72">
<SheetHeader>
<SheetTitle>Northwind</SheetTitle>
<SheetDescription>Revenue workspace</SheetDescription>
</SheetHeader>
<nav aria-label="Main" className="px-2">
<ul className="flex flex-col gap-0.5">
{PAGES.map(({ href, label, icon: Icon }) => (
<li key={href}>
<a
href={href}
aria-current={href === current ? "page" : undefined}
className={cn(
"flex h-9 items-center gap-2 rounded-lg px-2.5 text-sm text-muted-foreground focus-ring hover:bg-muted hover:text-foreground",
"aria-[current=page]:bg-brand-muted aria-[current=page]:font-medium aria-[current=page]:text-foreground"
)}
>
<Icon aria-hidden="true" className="size-4" />
{label}
</a>
</li>
))}
</ul>
</nav>
</SheetContent>
</Sheet>
)
}Long content
An order's twelve events scroll in a named region that takes the focus, between a title that stays and an action at the foot.
import { DownloadIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
const EVENTS = [
"Order placed by Harbour Logistics",
"Card ending 4242 charged $1,240.00",
"Confirmation email sent",
"Picked at the Leeds warehouse",
"Packed in two boxes",
"Handed to the courier",
"Out for delivery",
"Delivered, signed for by R. Okafor",
"Refund of $310.00 requested",
"Refund approved by Saoirse Byrne",
"Refund sent to the card ending 4242",
"Invoice INV-2041 reissued",
].map((text, index) => ({ text, when: `Sep ${12 + Math.floor(index / 2)}, ${String(9 + (index % 8)).padStart(2, "0")}:${index % 2 ? "40" : "05"}` }))
// Twelve events and a sheet a phone high. The title stays at the top and the
// action at the foot while the log scrolls between them. The log is a named
// region that takes the focus, so it scrolls from the keyboard too.
export default function SheetScroll() {
return (
<Sheet>
<SheetTrigger render={<Button variant="outline" />}>Order history</SheetTrigger>
<SheetContent className="gap-0 sm:max-w-sm">
<SheetHeader className="border-b border-rule">
<SheetTitle>Order NW-4821</SheetTitle>
<SheetDescription>Everything that happened to it, oldest first.</SheetDescription>
</SheetHeader>
<div role="region" aria-label="Order history" tabIndex={0} className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 focus-ring">
<ol className="flex flex-col">
{EVENTS.map((event) => (
<li key={event.text} className="flex flex-col gap-0.5 border-b border-border py-3 last:border-0">
<span className="text-sm">{event.text}</span>
<span className="text-xs text-muted-foreground tabular-nums">{event.when}</span>
</li>
))}
</ol>
</div>
<SheetFooter className="border-t border-rule">
<Button variant="outline">
<DownloadIcon aria-hidden="true" data-icon="inline-start" />
Download as CSV
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
)
}From the bottom
What a phone offers for three selected invoices, in thumb's reach; each action closes the sheet and the focus returns to the bar.
import { BellRingIcon, CircleCheckIcon, DownloadIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
const ACTIONS = [
{ label: "Send reminders", icon: BellRingIcon },
{ label: "Download PDFs", icon: DownloadIcon },
{ label: "Mark as paid", icon: CircleCheckIcon },
]
// What a phone offers for three selected invoices: a sheet from the bottom,
// in thumb's reach, with actions as full-width rows. It opens with the focus
// on the first action; each closes the sheet as it runs, and the focus goes
// back to the bar's button.
export default function SheetBottom() {
return (
<div className="flex w-full max-w-xs items-center justify-between rounded-lg border border-border px-3 py-2">
<span className="text-sm tabular-nums">3 selected</span>
<Sheet>
<SheetTrigger render={<Button size="sm" aria-label="Actions for 3 selected invoices" />}>Actions</SheetTrigger>
<SheetContent side="bottom" className="gap-2 rounded-t-xl pb-4">
<SheetHeader className="pb-2">
<SheetTitle>3 invoices selected</SheetTitle>
<SheetDescription>
<span className="font-mono">INV-2039</span>, <span className="font-mono">INV-2040</span> and <span className="font-mono">INV-2041</span>
</SheetDescription>
</SheetHeader>
<ul className="flex flex-col px-2">
{ACTIONS.map(({ label, icon: Icon }) => (
<li key={label}>
<SheetClose render={<Button variant="ghost" className="h-11 w-full justify-start gap-3 text-base" />}>
<Icon aria-hidden="true" className="size-5" />
{label}
</SheetClose>
</li>
))}
</ul>
</SheetContent>
</Sheet>
</div>
)
}A record from a list
One sheet for the whole list, opened with each row's record; finalFocus returns the reader to the row they came from.
"use client"
import * as React from "react"
import { avatarFor } from "@/lib/avatars"
import { getInitials } from "@/lib/format"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import { StatusBadge } from "@/components/ui/status-badge"
const PEOPLE = [
{ name: "Saoirse Byrne", email: "saoirse@northwind.example", role: "Admin", status: "active", seen: "Today, 09:12" },
{ name: "Tomás Ortega", email: "tomas@northwind.example", role: "Member", status: "active", seen: "Yesterday" },
{ name: "Hiro Tanaka", email: "hiro@northwind.example", role: "Billing", status: "invited", seen: "Never" },
]
function Face({ name, size }: { name: string; size?: "sm" | "lg" }) {
return (
<Avatar size={size} aria-hidden="true">
<AvatarImage src={avatarFor(name)} alt="" />
<AvatarFallback>{getInitials(name)}</AvatarFallback>
</Avatar>
)
}
// One sheet for the whole list, opened from each row's button with that
// row's record. finalFocus names the button that opened it, so closing the
// sheet puts the reader back on their row, not at the top of the list.
export default function SheetInspector() {
const [open, setOpen] = React.useState(false)
// Kept after closing, so the sheet slides out with its record still in it.
const [person, setPerson] = React.useState(PEOPLE[0])
const opener = React.useRef<HTMLButtonElement | null>(null)
return (
<>
<ul aria-label="Members" className="w-full max-w-sm divide-y divide-border rounded-lg border border-border">
{PEOPLE.map((row) => (
<li key={row.name} className="flex items-center gap-3 px-3 py-2">
<Face name={row.name} size="sm" />
<span className="min-w-0 flex-1 truncate text-sm">{row.name}</span>
<Button
variant="ghost"
size="sm"
aria-label={`View ${row.name}`}
onClick={(event) => {
opener.current = event.currentTarget
setPerson(row)
setOpen(true)
}}
>
View
</Button>
</li>
))}
</ul>
<Sheet open={open} onOpenChange={setOpen}>
<SheetContent finalFocus={opener} className="sm:max-w-sm">
<SheetHeader className="flex-row items-center gap-3">
<Face name={person.name} size="lg" />
<div className="flex flex-col gap-0.5">
<SheetTitle>{person.name}</SheetTitle>
<SheetDescription>{person.email}</SheetDescription>
</div>
</SheetHeader>
<dl className="grid grid-cols-[8rem_1fr] gap-y-3 px-4 text-sm">
<dt className="text-muted-foreground">Role</dt>
<dd>{person.role}</dd>
<dt className="text-muted-foreground">Status</dt>
<dd>
<StatusBadge status={person.status} size="sm" />
</dd>
<dt className="text-muted-foreground">Last seen</dt>
<dd className="tabular-nums">{person.seen}</dd>
</dl>
</SheetContent>
</Sheet>
</>
)
}Unsaved changes
Escape, the scrim and the close button all ask first when the form has an edit: keep editing, where the focus opens, or discard.
"use client"
import * as React from "react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
// Escape, the scrim and the close button all ask the same thing: close. With
// edits in the form the sheet answers with a question instead of losing them
// — keep editing, which is where the focus opens, or discard.
export default function SheetUnsaved() {
const [open, setOpen] = React.useState(false)
const [asking, setAsking] = React.useState(false)
const [saved, setSaved] = React.useState("Harbour Logistics Ltd.")
const [name, setName] = React.useState(saved)
const dirty = name !== saved
function close(keep: boolean) {
if (keep) setSaved(name)
else setName(saved)
setAsking(false)
setOpen(false)
}
return (
<Sheet open={open} onOpenChange={(next) => (next || !dirty ? setOpen(next) : setAsking(true))}>
<SheetTrigger render={<Button variant="outline" />}>Edit billing name</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Billing name</SheetTitle>
<SheetDescription>Printed on every invoice from the next one on.</SheetDescription>
</SheetHeader>
<div className="flex flex-col gap-2 px-4">
<Label htmlFor="sheet-unsaved-name">Name on invoices</Label>
<Input id="sheet-unsaved-name" value={name} onChange={(event) => setName(event.target.value)} />
</div>
<SheetFooter>
<Button disabled={!dirty} focusableWhenDisabled onClick={() => close(true)}>
Save
</Button>
</SheetFooter>
<AlertDialog open={asking} onOpenChange={setAsking}>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogTitle>Discard your edit?</AlertDialogTitle>
<AlertDialogDescription>The billing name goes back to “{saved}”.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep editing</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={() => close(false)}>
Discard
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</SheetContent>
</Sheet>
)
}Right to left
The navigation comes in from the right; the title starts there and the close button, named in Arabic, sits at the other end.
import { ChartColumnIcon, LayoutDashboardIcon, MenuIcon, ReceiptTextIcon, UsersIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
const PAGES = [
{ href: "/overview", label: "نظرة عامة", icon: LayoutDashboardIcon },
{ href: "/reports", label: "التقارير", icon: ChartColumnIcon },
{ href: "/customers", label: "العملاء", icon: UsersIcon },
{ href: "/billing/invoices", label: "الفواتير", icon: ReceiptTextIcon },
]
// In a right-to-left app the navigation lives on the right, so the sheet
// comes in from there. It is portalled out of the page, so it carries dir
// itself; the title starts at the right and the close button sits at the
// other end of the header, the inline end, clear of it, named in Arabic.
export default function SheetRtl() {
return (
<div dir="rtl" lang="ar">
<Sheet>
<SheetTrigger render={<Button variant="outline" size="icon" aria-label="فتح التنقل" />}>
<MenuIcon aria-hidden="true" />
</SheetTrigger>
<SheetContent side="right" dir="rtl" lang="ar" closeLabel="إغلاق" className="w-72 gap-2 sm:max-w-72">
<SheetHeader>
<SheetTitle>نورثويند</SheetTitle>
<SheetDescription>مساحة عمل الإيرادات</SheetDescription>
</SheetHeader>
<nav aria-label="الرئيسية" className="px-2">
<ul className="flex flex-col gap-0.5">
{PAGES.map(({ href, label, icon: Icon }) => (
<li key={href}>
<a
href={href}
aria-current={href === "/customers" ? "page" : undefined}
className="flex h-9 items-center gap-2 rounded-lg px-2.5 text-sm text-muted-foreground focus-ring hover:bg-muted hover:text-foreground aria-[current=page]:bg-brand-muted aria-[current=page]:font-medium aria-[current=page]:text-foreground"
>
<Icon aria-hidden="true" className="size-4" />
{label}
</a>
</li>
))}
</ul>
</nav>
</SheetContent>
</Sheet>
</div>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| SheetContent.side | "top" | "right" | "bottom" | "left" | "right" | The edge the sheet slides in from — in a right-to-left app, the navigation's is the right. |
| SheetContent.showCloseButton | boolean | true | Draws the close button in the corner; Escape and the scrim close it either way. |
| SheetContent.closeLabel | string | "Close" | The close button's name, in the sheet's own language. |
| SheetContent.finalFocus | React.RefObject<HTMLElement> | — | Where the focus goes when it closes, for a sheet opened from state rather than its trigger — the row's own button. |
| open / onOpenChange | boolean / (open: boolean) => void | — | Control it to ask before closing: keep open false out of onOpenChange while a form has edits. |
Dependencies
Registry
Source
"use client"
import * as React from "react"
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
return (
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-overlay transition-opacity duration-(--duration-base) ease-(--ease-standard) data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
showCloseButton = true,
closeLabel = "Close",
...props
}: SheetPrimitive.Popup.Props & {
side?: "top" | "right" | "bottom" | "left"
showCloseButton?: boolean
/** The close button's name, in the sheet's own language. */
closeLabel?: string
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Popup
data-slot="sheet-content"
data-side={side}
className={cn(
"fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-(--shadow-modal) transition duration-(--duration-slow) ease-(--ease-emphasized) data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
className
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close
data-slot="sheet-close"
render={
<Button
variant="ghost"
// A thumb needs 40px; the pointer size stays compact. At the
// inline end, so it never sits on a right-to-left title.
className="absolute end-3 top-3 max-md:end-2 max-md:top-2 max-md:size-10"
size="icon-sm"
/>
}
>
<XIcon
/>
<span className="sr-only">{closeLabel}</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Popup>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-0.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
/**
* A class naming one of the theme's type registers — type-display and its kin.
* A register sets the family, the weight and the tracking in one class, which
* the class merge cannot see as replacing the title's own type, so the two
* stayed side by side and the stylesheet's order decided between them.
*/
const TYPE_REGISTER = /(?:^|\s)type-[a-z]/
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
// The title's own type is a register too: one given to it replaces it
// whole — family, weight, size, tracking and ink — rather than sharing the
// element with it.
const ownType = typeof className === "string" && TYPE_REGISTER.test(className)
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn(
!ownType && "type-display text-xl text-foreground",
className
)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: SheetPrimitive.Description.Props) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}