Drawer

A panel that slides in from an edge. Built on Vaul. For RTL-safe side panels, prefer direction="start" / "end" — SonoJS resolves them to physical sides so Vaul's animation matches the visual edge.

Basic

Pick a delivery time — side drawer on desktop, bottom on mobile.

Installation

CLI
npx @sonojs/ui@latest add drawer
Also needsnpx @sonojs/ui@latest add sonner && npm i sonner vaul

Direction (RTL / LTR)

Vaul only understands physical left / right. SonoJS maps start / end with resolveEdgeSide before opening, and sets dir on the portaled content so text stays correct.

direction.tsx
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"
import { Button } from "@/components/ui/button"

// Prefer logical edges — they follow writing direction.
<Drawer direction="end">
  <DrawerTrigger asChild>
    <Button variant="outline">Open from end</Button>
  </DrawerTrigger>
  <DrawerContent>…</DrawerContent>
</Drawer>

// Physical left / right never flip (escape hatch).
<Drawer direction="right">…</Drawer>

Composition

structure
Drawer
├── DrawerTrigger
└── DrawerContent
    ├── DrawerHeader
    │   ├── DrawerTitle
    │   └── DrawerDescription
    └── DrawerFooter

Usage

drawer.tsx
"use client"

import * as React from "react"
import { toast } from "sonner"

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
import { useIsMobile } from "@/hooks/use-mobile"

export function Example() {
  const [open, setOpen] = React.useState(false)
  const [deliveryTime, setDeliveryTime] = React.useState("asap")
  const isMobile = useIsMobile()

  return (
    <Drawer
      open={open}
      onOpenChange={setOpen}
      direction={isMobile ? "bottom" : "end"}
    >
      <DrawerTrigger asChild>
        <Button variant="outline">Open Drawer</Button>
      </DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Pick a delivery time</DrawerTitle>
          <DrawerDescription>
            We&apos;ll prepare your order as soon as possible.
          </DrawerDescription>
        </DrawerHeader>
        {/* RadioGroup of delivery slots… */}
        <DrawerFooter>
          <Button
            onClick={() => {
              setOpen(false)
              toast("Delivery time confirmed")
            }}
          >
            Confirm Delivery Time
          </Button>
          <DrawerClose asChild>
            <Button variant="outline">Cancel</Button>
          </DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}

Props

PropTypeDefaultDescription
direction"top" · "bottom" · "left" · "right" · "start" · "end""bottom"On Drawer — prefer start/end for RTL. left/right are physical and do not flip.
open / onOpenChangeboolean / (open: boolean) => void—On Drawer — controlled open state.
asChildbooleanfalseOn DrawerTrigger / DrawerClose — merge props into the child (e.g. Button).
shouldScaleBackgroundbooleantrueOn Drawer — Vaul scales the page behind the drawer.