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/radix/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

DrawerTrigger

PropType
AttributeType

DrawerPortal

PropType
AttributeType

DrawerClose

PropType
AttributeType

DrawerOverlay

PropType
AttributeType

DrawerContent

PropType
AttributeType

DrawerHeader

PropType
AttributeType

DrawerFooter

PropType
AttributeType

DrawerTitle

PropType
AttributeType

DrawerDescription

PropType
AttributeType