Build websites 10x faster with HextaUI Blocks — Learn more
UI/UI

Drawer

A customizable drawer component built on top of Vaul with smooth animations and flexible positioning.

import { 
  Drawer,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerDescription,
  DrawerFooter,
  DrawerTrigger,
  DrawerClose,
  DrawerCloseButton
} from "@/components/ui/Drawer";

<Drawer>
  <DrawerTrigger asChild>
    <Button variant="outline">Open Drawer</Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Basic Drawer</DrawerTitle>
      <DrawerDescription>
        This is a basic drawer component.
      </DrawerDescription>
    </DrawerHeader>
    <DrawerFooter>
      <Button>Submit</Button>
      <DrawerClose asChild>
        <Button variant="outline">Cancel</Button>
      </DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

This drawer component is built on top of Vaul created by Emil Kowalski

Installation

Install following dependencies:

npm install vaul
pnpm add vaul
yarn add vaul
bun add vaul

Copy and paste the following code into your project.

components/ui/Drawer/drawer.tsx
"use client";

import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils";

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.Overlay>) {
  return (
    <DrawerPrimitive.Overlay
      data-slot="drawer-overlay"
      className={cn(
        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
        className,
      )}
      {...props}
    />
  );
}

function DrawerContent({
  className,
  children,
  ...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
  return (
    <DrawerPortal data-slot="drawer-portal">
      <DrawerOverlay />
      <DrawerPrimitive.Content
        data-slot="drawer-content"
        className={cn(
          "group/drawer-content bg-[hsl(var(--hu-card))] text-[hsl(var(--hu-foreground))] border-[hsl(var(--hu-border))] fixed z-50 flex h-auto flex-col",
          "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-xl data-[vaul-drawer-direction=top]:border-b",
          "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-xl data-[vaul-drawer-direction=bottom]:border-t",
          "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:rounded-l-xl data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
          "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:rounded-r-xl data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
          className,
        )}
        {...props}
      >
        <div className="bg-[hsl(var(--hu-muted))] mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
        {children}
      </DrawerPrimitive.Content>
    </DrawerPortal>
  );
}

function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="drawer-header"
      className={cn(
        "flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
        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-[hsl(var(--hu-foreground))] font-semibold",
        className,
      )}
      {...props}
    />
  );
}

function DrawerDescription({
  className,
  ...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
  return (
    <DrawerPrimitive.Description
      data-slot="drawer-description"
      className={cn(
        "text-[hsl(var(--hu-muted-foreground))] text-sm",
        className,
      )}
      {...props}
    />
  );
}

function DrawerCloseButton({
  className,
  ...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
  return (
    <DrawerPrimitive.Close
      data-slot="drawer-close-button"
      className={cn(
        "absolute right-4 top-4 rounded-lg opacity-70 ring-offset-[hsl(var(--hu-background))] transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-[hsl(var(--hu-ring))] focus:ring-offset-2 disabled:pointer-events-none",
        className,
      )}
      {...props}
    >
      <svg
        width="15"
        height="15"
        viewBox="0 0 15 15"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
        className="h-4 w-4"
      >
        <path
          d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
          fill="currentColor"
          fillRule="evenodd"
          clipRule="evenodd"
        />
      </svg>
      <span className="sr-only">Close</span>
    </DrawerPrimitive.Close>
  );
}

export {
  Drawer,
  DrawerPortal,
  DrawerOverlay,
  DrawerTrigger,
  DrawerClose,
  DrawerContent,
  DrawerHeader,
  DrawerFooter,
  DrawerTitle,
  DrawerDescription,
  DrawerCloseButton,
};
npx hextaui@latest add drawer
pnpm dlx hextaui@latest add drawer
yarn dlx hextaui@latest add drawer
bun x hextaui@latest add drawer

Usage

import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/Drawer";
<Drawer>
  <DrawerTrigger>Open</DrawerTrigger>
  <DrawerContent>Content goes here</DrawerContent>
</Drawer>

Examples

Drawer Sides

{/* Bottom drawer (default) */}
<Drawer>
  <DrawerTrigger asChild>
    <Button variant="outline">Bottom</Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Bottom Drawer</DrawerTitle>
    </DrawerHeader>
  </DrawerContent>
</Drawer>

{/* Other directions */}
<Drawer direction="top">
  <DrawerTrigger asChild>
    <Button variant="outline">Top</Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Top Drawer</DrawerTitle>
    </DrawerHeader>
  </DrawerContent>
</Drawer>

<Drawer direction="left">
  <DrawerTrigger asChild>
    <Button variant="outline">Left</Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Left Drawer</DrawerTitle>
    </DrawerHeader>
  </DrawerContent>
</Drawer>

<Drawer direction="right">
  <DrawerTrigger asChild>
    <Button variant="outline">Right</Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Right Drawer</DrawerTitle>
    </DrawerHeader>
  </DrawerContent>
</Drawer>

With Close Button

<DrawerContent>
  <DrawerCloseButton />
  <DrawerHeader>
    <DrawerTitle>Title</DrawerTitle>
  </DrawerHeader>
</DrawerContent>

Form Example

<Drawer>
  <DrawerTrigger asChild>
    <Button>Subscribe</Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Subscribe to Newsletter</DrawerTitle>
    </DrawerHeader>
    <div className="p-4">
      <input type="email" placeholder="Enter email" />
    </div>
    <DrawerFooter>
      <Button>Subscribe</Button>
      <DrawerClose asChild>
        <Button variant="outline">Cancel</Button>
      </DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

Rich Content

<DrawerContent>
  <DrawerHeader>
    <DrawerTitle>Product Details</DrawerTitle>
    <DrawerDescription>Complete information</DrawerDescription>
  </DrawerHeader>
  <div className="p-4">{/* Rich content */}</div>
  <DrawerFooter>
    <Button>Add to Cart</Button>
  </DrawerFooter>
</DrawerContent>

Props

PropTypeDefault
onOpenChange?
(open: boolean) => void
-
open?
boolean
-
modal?
boolean
true
direction?
"top" | "bottom" | "left" | "right"
"bottom"
Edit on GitHub

Last updated on