Skip to content

shadcn.io is not affiliated with official shadcn/ui

Shadcn Drawer for React

A sliding panel with swipe support.

Installation

bunx --bun shadcn@latest add https://kit.dev/r/aria/drawer.json

Anatomy

DrawerPositioner
DrawerPositioner
DrawerTitle
DrawerDescription
DrawerTrigger
DrawerOverlay
DrawerGrabber
└── DrawerGrabberIndicator
DrawerClose
DrawerSwipeArea

Usage

import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer";
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldLabel,
  FieldTitle,
} from "@/components/ui/field";
import {
  RadioGroup,
  RadioGroupItem,
} from "@/components/ui/radio-group";
import { toast } from "@/components/ui/toast";
import { useIsMobile } from "@/hooks/use-is-mobile";
const deliveryTimes = [
  {
    badge: "Fastest",
    description: "25–35 min · Driver assigned now",
    id: "delivery-asap",
    label: "Standard delivery",
    value: "asap",
  },
  {
    description: "Prep starts at 4:45 PM",
    id: "delivery-5-00",
    label: "5:00 PM – 5:15 PM",
    value: "5-00",
  },
  {
    description: "Good if you're heading home",
    id: "delivery-5-30",
    label: "5:30 PM – 5:45 PM",
    value: "5-30",
  },
  {
    description: "Most popular · High demand",
    id: "delivery-6-00",
    label: "6:00 PM – 6:15 PM",
    value: "6-00",
  },
  {
    description: "Last slot before kitchen closes",
    id: "delivery-6-30",
    label: "6:30 PM – 6:45 PM",
    value: "6-30",
  },
];

const DrawerDemo = () => {
  const [open, setOpen] = useState(false);
  const [deliveryTime, setDeliveryTime] = useState("asap");
  const isMobile = useIsMobile();

  const handleOpenChange = (details: { open: boolean }) => {
    setOpen(details.open);
  };

  const handleDeliveryTimeChange = (details: { value: string | null }) => {
    if (details.value !== null) {
      setDeliveryTime(details.value);
    }
  };

  const handleConfirm = () => {
    const selected = deliveryTimes.find((time) => time.value === deliveryTime);

    if (!selected) {
      return;
    }

    setOpen(false);
    toast.create({
      description: selected.label,
      title: "Delivery time confirmed",
    });
  };

  return (
    <Drawer
      onOpenChange={handleOpenChange}
      open={open}
      swipeDirection={isMobile ? "down" : "right"}
    >
      <DrawerTrigger asChild>
        <Button variant="secondary">Open Drawer</Button>
      </DrawerTrigger>
      <DrawerContent showBar={isMobile}>
        <DrawerHeader>
          <DrawerTitle>Pick a delivery time</DrawerTitle>
          <DrawerDescription>
            We&apos;ll prepare your order as soon as possible.
          </DrawerDescription>
        </DrawerHeader>
        <div className="scroll-fade flex-1 overflow-y-auto p-4">
          <RadioGroup
            className="gap-2"
            onValueChange={handleDeliveryTimeChange}
            value={deliveryTime}
          >
            {deliveryTimes.map((time) => (
              <FieldLabel key={time.value}>
                <Field orientation="horizontal">
                  <FieldContent>
                    <FieldTitle className="flex items-center gap-2">
                      {time.label}
                      {time.badge ? (
                        <Badge variant="secondary">{time.badge}</Badge>
                      ) : null}
                    </FieldTitle>
                    <FieldDescription>{time.description}</FieldDescription>
                  </FieldContent>
                  <RadioGroupItem value={time.value}>
                    <span className="sr-only">{time.label}</span>
                  </RadioGroupItem>
                </Field>
              </FieldLabel>
            ))}
          </RadioGroup>
        </div>
        <DrawerFooter>
          <Button className="h-[34px]" onClick={handleConfirm}>
            Confirm Delivery Time
          </Button>
          <DrawerClose asChild>
            <Button variant="outline">Cancel</Button>
          </DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  );
};

Accessibility

KeyDescription
Enter
Activate the focused control.
Space
Activate the focused control.
End
Go to the end.

Demos

Sides

Position the drawer with swipeDirection.

Swipe Handle

Show a swipe handle with showBar on DrawerContent.

Nested

Open drawers from inside another drawer.

Non Modal

Allow page interaction while the drawer is open with modal={false}.

Snap Points

Snap a vertical drawer to preset heights with snapPoints.

Dialog

Combine Dialog and Drawer for a responsive edit form.

API Reference

Drawer

PropType
AttributeType

DrawerContent

div

AttributeType
CSS variableType
--bleedstring
--closed-transformstring
--color-popoverstring
--drawer-content-heightstring
--drawer-swipe-movement-xstring
--drawer-swipe-movement-ystring
--drawer-swipe-progressstring
--drawer-swipe-strengthstring
--nested-drawersstring
--peekstring
--stack-heightstring
--stack-peek-offsetstring
--stack-progressstring
--stack-scalestring
--stack-scale-basestring
--stack-shrinkstring
--stack-stepstring

DrawerTitle

h2

PropType
AttributeType

DrawerDescription

div

PropType
AttributeType

DrawerTrigger

button

PropType
AttributeType

DrawerOverlay

div

PropType
AttributeType
CSS variableType
--drawer-swipe-progressstring
--drawer-swipe-strengthstring

DrawerPortal

PropType
AttributeType

DrawerClose

AttributeType

DrawerSwipeHandle

PropType
AttributeType

DrawerHeader

PropType
AttributeType

DrawerFooter

PropType
AttributeType

useDrawer