Drawer
A wrapper for Base UI Drawer with side support, swipe-to-dismiss, snap points, async loading, and nested drawer support.
Basic
Sides
Advanced
Controlled
Async
Installation
npx shadcn@latest add @glrk-ui/drawerIf 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 { Drawer as DrawerPrimitive } from '@base-ui/react/drawer'
import { Loader } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button, buttonVariants } from '@/components/ui/button'
type DrawerSide = 'bottom' | 'top' | 'left' | 'right'
const sideClasses: Record<DrawerSide, string> = {
bottom: 'inset-x-0 bottom-0 mt-24 max-h-[80vh] rounded-t-xl border-t',
top: 'inset-x-0 top-0 mb-24 max-h-[80vh] rounded-b-xl border-b',
left: 'inset-y-0 left-0 w-3/4 rounded-r-xl border-r sm:max-w-sm',
right: 'inset-y-0 right-0 w-3/4 rounded-l-xl border-l sm:max-w-sm',
}
function Drawer(props: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger(props: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal(props: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose(props: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Backdrop>) {
return (
<DrawerPrimitive.Backdrop
data-slot="drawer-overlay"
className={cn(
'fixed inset-0 bg-black/10 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0',
className,
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
side = 'bottom',
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content> & { side?: DrawerSide }) {
return (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Viewport>
<DrawerPrimitive.Popup>
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
'group/drawer-content fixed flex h-auto flex-col bg-background text-sm',
sideClasses[side],
className,
)}
{...props}
>
{children}
</DrawerPrimitive.Content>
</DrawerPrimitive.Popup>
</DrawerPrimitive.Viewport>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="drawer-header"
className={cn('flex flex-col gap-0.5 p-4', className)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="drawer-footer"
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props}
/>
)
}
function DrawerTitle({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn('text-base font-medium text-foreground', className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
)
}
type DrawerFooterWrapperProps = {
cancel?: React.ReactNode
action?: React.ReactNode
loading?: boolean
footerCls?: string
actionCls?: string
cancelCls?: string
onAction?: () => void
onCancel?: () => void
}
function DrawerFooterWrapper({
cancel,
action,
loading = false,
footerCls,
actionCls,
cancelCls,
onAction = () => {},
onCancel = () => {},
}: DrawerFooterWrapperProps) {
return (
<DrawerFooter className={cn(footerCls)}>
{cancel && (
<DrawerClose
className={cn(buttonVariants({ variant: 'outline' }), cancelCls)}
onClick={onCancel}
disabled={loading}
>
{cancel}
</DrawerClose>
)}
{action && (
<Button onClick={onAction} className={cn(actionCls)} disabled={loading}>
{loading && <Loader className="animate-spin" />}
{action}
</Button>
)}
</DrawerFooter>
)
}
type DrawerWrapperProps = {
title?: React.ReactNode
trigger?: React.ReactNode
triggerCls?: string
triggerProps?: Omit<React.ComponentProps<typeof DrawerPrimitive.Trigger>, 'children' | 'className'>
children?: React.ReactNode
description?: React.ReactNode
descriptionCls?: string
contentCls?: string
headerCls?: string
titleCls?: string
side?: DrawerSide
} & DrawerFooterWrapperProps
function DrawerWrapper({
trigger,
title,
description,
children,
triggerCls,
triggerProps,
contentCls,
headerCls,
titleCls,
descriptionCls,
cancel = 'Cancel',
action,
loading = false,
footerCls,
actionCls,
cancelCls,
onAction,
onCancel,
onOpenChange,
side,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root> & DrawerWrapperProps) {
return (
<Drawer
{...props}
onOpenChange={(open, eventDetails) => {
if (!open && loading) {
eventDetails.cancel()
return
}
onOpenChange?.(open, eventDetails)
}}
>
{trigger && (
<DrawerTrigger className={cn(triggerCls)} {...triggerProps}>
{trigger}
</DrawerTrigger>
)}
<DrawerContent side={side} className={cn(contentCls)}>
<DrawerHeader className={cn(headerCls)}>
<DrawerTitle className={cn(titleCls)}>{title}</DrawerTitle>
{description && (
<DrawerDescription className={cn(descriptionCls)}>{description}</DrawerDescription>
)}
</DrawerHeader>
{children}
{(!!cancel || !!action) && (
<DrawerFooterWrapper
cancel={cancel}
action={action}
loading={loading}
footerCls={footerCls}
actionCls={actionCls}
cancelCls={cancelCls}
onAction={onAction}
onCancel={onCancel}
/>
)}
</DrawerContent>
</Drawer>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
DrawerWrapper,
DrawerFooterWrapper,
}
Usage
Basic
import { DrawerWrapper } from "@/components/ui/drawer"
export function Basic() {
return (
<DrawerWrapper
trigger="Open"
triggerCls={buttonVariants({ variant: 'outline' })}
title="Basic drawer"
description="Swipe down to dismiss."
action="Confirm"
/>
)
}Sides
Use side to set which edge the drawer slides from. Pair with swipeDirection to match:
<DrawerWrapper side="bottom" swipeDirection="down" trigger="Bottom" title="Bottom drawer" />
<DrawerWrapper side="top" swipeDirection="up" trigger="Top" title="Top drawer" />
<DrawerWrapper side="left" swipeDirection="left" trigger="Left" title="Left drawer" />
<DrawerWrapper side="right" swipeDirection="right" trigger="Right" title="Right drawer" />Snap points
<DrawerWrapper
trigger="Snap Points"
title="Snap points"
description="Drawer snaps to 40% or 100% height. Drag to switch."
snapPoints={[0.4, 1]}
action="Confirm"
/>Non-modal
Page content stays interactive while the drawer is open:
<DrawerWrapper
trigger="Non-modal"
modal={false}
title="Non-modal drawer"
description="The page behind remains interactive."
action="Close"
/>Nested drawers
<DrawerWrapper trigger="Open outer" title="Outer drawer" action="">
<DrawerWrapper
trigger="Open inner"
title="Inner drawer"
description="Nested inside the outer drawer."
action="Confirm"
/>
</DrawerWrapper>Controlled
export function Controlled() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Open</button>
<DrawerWrapper
open={open}
onOpenChange={setOpen}
title="Controlled drawer"
action="Confirm"
onAction={() => setOpen(false)}
/>
</>
)
}Async action with loading state
export function AsyncSubmit() {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
async function handleSubmit() {
setLoading(true)
await submitData()
setLoading(false)
setOpen(false)
}
return (
<DrawerWrapper
open={open}
onOpenChange={setOpen}
trigger="Submit"
title="Submit form?"
action="Submit"
loading={loading}
onAction={handleSubmit}
/>
)
}Custom footer content
<DrawerWrapper trigger="Open" title="Custom footer" action="" cancel="">
<DrawerFooter>
<DrawerClose className={buttonVariants({ variant: 'outline' })}>Cancel</DrawerClose>
<Button onClick={handleAction}>Confirm</Button>
</DrawerFooter>
</DrawerWrapper>Reference
DrawerFooterWrapper
DrawerWrapper
Prop
Type