Sheet
A wrapper for Base UI Dialog styled as a side panel, with side support, async loading, nested sheets, and custom trigger composition.
Basic
Sides
Advanced
Controlled
Async
Installation
npx shadcn@latest add @glrk-ui/sheetIf you haven't set up the prerequisites yet, check out Prerequest section.
Copy and paste the following code into your project.
'use client'
import * as React from 'react'
import { Dialog as SheetPrimitive } from '@base-ui/react/dialog'
import { Loader, XIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
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 bg-black/10 transition-opacity duration-150 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,
...props
}: SheetPrimitive.Popup.Props & {
side?: 'top' | 'right' | 'bottom' | 'left'
showCloseButton?: boolean
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Popup
data-slot="sheet-content"
data-side={side}
className={cn(
'fixed flex flex-col gap-4 bg-background bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out 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-10 data-[side=bottom]:data-starting-style:translate-y-10 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-10 data-[side=left]:data-starting-style:-translate-x-10 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-10 data-[side=right]:data-starting-style:translate-x-10 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-10 data-[side=top]:data-starting-style:-translate-y-10 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" className="absolute top-3 right-3" size="icon-sm" />}
>
<XIcon />
<span className="sr-only">Close</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}
/>
)
}
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn('text-base font-medium 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}
/>
)
}
type SheetFooterWrapperProps = {
cancel?: React.ReactNode
action?: React.ReactNode
loading?: boolean
footerCls?: string
actionCls?: string
cancelCls?: string
onAction?: () => void
onCancel?: () => void
}
function SheetFooterWrapper({
cancel,
action,
loading = false,
footerCls,
actionCls,
cancelCls,
onAction = () => {},
onCancel = () => {},
}: SheetFooterWrapperProps) {
return (
<SheetFooter className={footerCls}>
{cancel && (
<SheetClose
render={
<Button variant="secondary" onClick={onCancel} className={cn('border', cancelCls)} disabled={loading} />
}
>
{cancel}
</SheetClose>
)}
{action && (
<Button onClick={onAction} className={cn(actionCls)} disabled={loading}>
{loading && <Loader className="animate-spin" />}
{action}
</Button>
)}
</SheetFooter>
)
}
type SheetWrapperProps = {
title?: React.ReactNode
trigger?: React.ReactNode
triggerCls?: string
triggerProps?: Omit<SheetPrimitive.Trigger.Props, 'children' | 'className'>
children?: React.ReactNode
description?: React.ReactNode
descriptionCls?: string
contentCls?: string
headerCls?: string
titleCls?: string
side?: 'top' | 'bottom' | 'right' | 'left'
showCloseButton?: boolean
} & SheetFooterWrapperProps
function SheetWrapper({
trigger,
title,
description,
children,
contentCls,
headerCls,
titleCls,
descriptionCls,
cancel = 'Cancel',
action,
loading = false,
triggerCls,
triggerProps,
footerCls,
actionCls,
cancelCls,
onAction,
onCancel,
onOpenChange,
side = 'right',
showCloseButton,
...props
}: React.ComponentProps<typeof SheetPrimitive.Root> & SheetWrapperProps) {
return (
<Sheet
{...props}
onOpenChange={(open, eventDetails) => {
if (!open && loading) {
eventDetails.cancel()
return
}
onOpenChange?.(open, eventDetails)
}}
>
{trigger && (
<SheetTrigger className={cn(triggerCls)} {...triggerProps}>
{trigger}
</SheetTrigger>
)}
<SheetContent side={side} className={cn(contentCls)} showCloseButton={showCloseButton}>
<SheetHeader className={cn(headerCls)}>
<SheetTitle className={cn(titleCls)}>{title}</SheetTitle>
{description && (
<SheetDescription className={cn(descriptionCls)}>{description}</SheetDescription>
)}
</SheetHeader>
{children}
{(!!cancel || !!action) && (
<SheetFooterWrapper
cancel={cancel}
action={action}
loading={loading}
footerCls={footerCls}
actionCls={actionCls}
cancelCls={cancelCls}
onAction={onAction}
onCancel={onCancel}
/>
)}
</SheetContent>
</Sheet>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
SheetWrapper,
SheetFooterWrapper,
SheetPortal,
SheetOverlay,
}
Usage
Basic
import { SheetWrapper } from "@/components/ui/sheet"
export function Basic() {
return (
<SheetWrapper
trigger="Open"
triggerCls={buttonVariants({ variant: 'outline' })}
title="Settings"
description="Adjust your preferences."
action="Save"
/>
)
}Sides
<SheetWrapper side="right" trigger="Right" title="Right sheet" />
<SheetWrapper side="left" trigger="Left" title="Left sheet" />
<SheetWrapper side="top" trigger="Top" title="Top sheet" />
<SheetWrapper side="bottom" trigger="Bottom" title="Bottom sheet" />Hide close button
<SheetWrapper
trigger="Open"
title="No close button"
showCloseButton={false}
action="Done"
/>Custom element trigger
<SheetWrapper
trigger="Open settings"
triggerProps={{
render: <a href="#" />,
nativeButton: false,
}}
triggerCls="text-sm text-primary underline underline-offset-4"
title="Settings"
/>Controlled
export function Controlled() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Open</button>
<SheetWrapper
open={open}
onOpenChange={setOpen}
title="Controlled sheet"
action="Save"
onAction={() => setOpen(false)}
/>
</>
)
}Async action with loading state
export function AsyncSave() {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
async function handleSave() {
setLoading(true)
await saveSettings()
setLoading(false)
setOpen(false)
}
return (
<SheetWrapper
open={open}
onOpenChange={setOpen}
trigger="Settings"
title="Save settings?"
action="Save"
loading={loading}
onAction={handleSave}
/>
)
}Nested sheets
<SheetWrapper trigger="Open outer" side="right" title="Outer sheet" action="">
<SheetWrapper
trigger="Open inner"
side="left"
title="Inner sheet"
description="Slides from the opposite side."
action="Confirm"
/>
</SheetWrapper>Custom footer content
<SheetWrapper trigger="Open" title="Custom footer" action="" cancel="">
<SheetFooter>
<SheetClose render={<Button variant="secondary" className="border" />}>Cancel</SheetClose>
<Button onClick={handleAction}>Save</Button>
</SheetFooter>
</SheetWrapper>Reference
SheetFooterWrapper
SheetWrapper
Prop
Type