Drawer

PreviousNext

A drawer component slides in from the edge of the screen to show additional content or navigation options.

Component drawer-demo not found in registry.

Installation

Install the following dependencies:

pnpm add @radix-ui/react-dialog lucide-react

Copy and paste the following code into your project.

// @Diar Muradi Drawer v0.0

"use client"

import * as React from "react"
import { tv, type VariantProps } from "@diarmuradi/ui/lib/index"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"

// Component identifier names
const DRAWER_ROOT_NAME = "DrawerRoot"
const DRAWER_TRIGGER_NAME = "DrawerTrigger"
const DRAWER_CLOSE_NAME = "DrawerClose"
const DRAWER_OVERLAY_NAME = "DrawerOverlay"
const DRAWER_CONTENT_NAME = "DrawerContent"
const DRAWER_HEADER_NAME = "DrawerHeader"
const DRAWER_TITLE_NAME = "DrawerTitle"
const DRAWER_BODY_NAME = "DrawerBody"
const DRAWER_FOOTER_NAME = "DrawerFooter"

export const drawerVariants = tv({
  slots: {
    overlay: [
      // base
      "fixed inset-0 z-50 grid grid-cols-1 place-items-end overflow-hidden backdrop-blur-[10px] bg-foreground/25",
      // animation
      "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
    ],
    content: [
      // base
      "size-full max-w-[400px] overflow-y-auto",
      "border-l border-border bg-background",
      // animation
      "data-[state=open]:duration-200 data-[state=open]:ease-out data-[state=open]:animate-in",
      "data-[state=closed]:duration-200 data-[state=closed]:ease-in data-[state=closed]:animate-out",
      "data-[state=open]:slide-in-from-right-full",
      "data-[state=closed]:slide-out-to-right-full",
      // focus
      "focus:outline-none",
    ],
    header: ["flex items-center gap-3 border-b border-border p-5"],
    title: ["flex-1 text-sm font-semibold text-foreground"],
    body: ["flex-1 p-5"],
    footer: ["flex items-center gap-4 border-t border-border p-5"],
    closeButton: [
      "inline-flex size-8 items-center justify-center rounded-md border border-transparent",
      "text-muted-foreground transition-colors duration-200",
      "hover:bg-muted hover:text-foreground",
      "focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2",
    ],
  },
  variants: {
    side: {
      right: {
        overlay: "place-items-end",
        content: [
          "border-l",
          "data-[state=open]:slide-in-from-right-full",
          "data-[state=closed]:slide-out-to-right-full",
        ],
      },
      left: {
        overlay: "place-items-start",
        content: [
          "border-r border-l-0",
          "data-[state=open]:slide-in-from-left-full",
          "data-[state=closed]:slide-out-to-left-full",
        ],
      },
    },
  },
  defaultVariants: {
    side: "right",
  },
})

// Root component
const DrawerRoot = DialogPrimitive.Root
DrawerRoot.displayName = DRAWER_ROOT_NAME

// Trigger component
const DrawerTrigger = DialogPrimitive.Trigger
DrawerTrigger.displayName = DRAWER_TRIGGER_NAME

// Close component
const DrawerClose = DialogPrimitive.Close
DrawerClose.displayName = DRAWER_CLOSE_NAME

// Portal component
const DrawerPortal = DialogPrimitive.Portal
DrawerPortal.displayName = "DrawerPortal"

// Overlay component
const DrawerOverlay = React.forwardRef<
  React.ComponentRef<typeof DialogPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> &
    VariantProps<typeof drawerVariants>
>(({ className, side, ...rest }, forwardedRef) => {
  const { overlay } = drawerVariants({ side })

  return (
    <DialogPrimitive.Overlay
      ref={forwardedRef}
      className={overlay({ className })}
      {...rest}
    />
  )
})
DrawerOverlay.displayName = DRAWER_OVERLAY_NAME

// Content component
const DrawerContent = React.forwardRef<
  React.ComponentRef<typeof DialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &
    VariantProps<typeof drawerVariants>
>(({ className, children, side, ...rest }, forwardedRef) => {
  const { content } = drawerVariants({ side })

  return (
    <DrawerPortal>
      <DrawerOverlay side={side}>
        <DialogPrimitive.Content
          ref={forwardedRef}
          className={content({ className })}
          {...rest}
        >
          <div className="relative flex size-full flex-col">{children}</div>
        </DialogPrimitive.Content>
      </DrawerOverlay>
    </DrawerPortal>
  )
})
DrawerContent.displayName = DRAWER_CONTENT_NAME

// Header component
const DrawerHeader = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement> & {
    showCloseButton?: boolean
  } & VariantProps<typeof drawerVariants>
>(({ className, children, showCloseButton = true, ...rest }, forwardedRef) => {
  const { header, closeButton } = drawerVariants()

  return (
    <div ref={forwardedRef} className={header({ className })} {...rest}>
      {children}

      {showCloseButton && (
        <DrawerClose className={closeButton()}>
          <X className="size-4" />
          <span className="sr-only">Close</span>
        </DrawerClose>
      )}
    </div>
  )
})
DrawerHeader.displayName = DRAWER_HEADER_NAME

// Title component
const DrawerTitle = React.forwardRef<
  React.ComponentRef<typeof DialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> &
    VariantProps<typeof drawerVariants>
>(({ className, ...rest }, forwardedRef) => {
  const { title } = drawerVariants()

  return (
    <DialogPrimitive.Title
      ref={forwardedRef}
      className={title({ className })}
      {...rest}
    />
  )
})
DrawerTitle.displayName = DRAWER_TITLE_NAME

// Body component
const DrawerBody = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof drawerVariants>
>(({ className, children, ...rest }, forwardedRef) => {
  const { body } = drawerVariants()

  return (
    <div ref={forwardedRef} className={body({ className })} {...rest}>
      {children}
    </div>
  )
})
DrawerBody.displayName = DRAWER_BODY_NAME

// Footer component
const DrawerFooter = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof drawerVariants>
>(({ className, ...rest }, forwardedRef) => {
  const { footer } = drawerVariants()

  return <div ref={forwardedRef} className={footer({ className })} {...rest} />
})
DrawerFooter.displayName = DRAWER_FOOTER_NAME

export {
  DrawerRoot as Root,
  DrawerTrigger as Trigger,
  DrawerClose as Close,
  DrawerContent as Content,
  DrawerHeader as Header,
  DrawerTitle as Title,
  DrawerBody as Body,
  DrawerFooter as Footer,
}

Update the import paths to match your project setup.

Usage

import * as Drawer from "@/components/ui/drawer"
 
export default function Example() {
  return (
    <Drawer.Root>
      <Drawer.Trigger asChild>
        <button>Open Drawer</button>
      </Drawer.Trigger>
      <Drawer.Content>
        <Drawer.Header>
          <Drawer.Title>Drawer Title</Drawer.Title>
        </Drawer.Header>
        <Drawer.Body>
          <p>Drawer content goes here.</p>
        </Drawer.Body>
        <Drawer.Footer>
          <button>Action</button>
        </Drawer.Footer>
      </Drawer.Content>
    </Drawer.Root>
  )
}

Examples

Basic Drawer

Component drawer-demo not found in registry.

Left Side Drawer

Component drawer-left not found in registry.

Without Header

Component drawer-without-header not found in registry.