"use client";
import * as React from "react";
import { useIsMobile } from "@/hooks/use-mobile";
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";
const deliveryTimes = [
{
value: "asap",
id: "delivery-asap",
label: "Standard delivery",
description: "25–35 min · Driver assigned now",
badge: "Fastest",
},
{
value: "5-00",
id: "delivery-5-00",
label: "5:00 PM – 5:15 PM",
description: "Prep starts at 4:45 PM",
},
{
value: "5-30",
id: "delivery-5-30",
label: "5:30 PM – 5:45 PM",
description: "Good if you're heading home",
},
{
value: "6-00",
id: "delivery-6-00",
label: "6:00 PM – 6:15 PM",
description: "Most popular · High demand",
},
{
value: "6-30",
id: "delivery-6-30",
label: "6:30 PM – 6:45 PM",
description: "Last slot before kitchen closes",
},
];
export function DrawerDemo() {
const [open, setOpen] = React.useState(false);
const [deliveryTime, setDeliveryTime] = React.useState("asap");
const isMobile = useIsMobile();
function handleConfirm() {
const selected = deliveryTimes.find((time) => time.value === deliveryTime);
if (!selected) {
return;
}
setOpen(false);
toast.add({
title: "Delivery time confirmed",
description: selected.label,
});
}
return (
<Drawer
open={open}
onOpenChange={setOpen}
showSwipeHandle={isMobile}
swipeDirection={isMobile ? "down" : "right"}
>
<DrawerTrigger render={<Button variant="secondary" />}>
Open Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Pick a delivery time</DrawerTitle>
<DrawerDescription>
We'll prepare your order as soon as possible.
</DrawerDescription>
</DrawerHeader>
<div className="scroll-fade flex-1 overflow-y-auto p-4">
<RadioGroup
variant="secondary"
value={deliveryTime}
onValueChange={setDeliveryTime}
className="gap-2"
>
{deliveryTimes.map((time) => (
<FieldLabel key={time.value} htmlFor={time.id}>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle className="flex items-center gap-2">
{time.label}
{time.badge ? (
<Badge variant="primary">{time.badge}</Badge>
) : null}
</FieldTitle>
<FieldDescription>{time.description}</FieldDescription>
</FieldContent>
<RadioGroupItem value={time.value} id={time.id} />
</Field>
</FieldLabel>
))}
</RadioGroup>
</div>
<DrawerFooter>
<Button onClick={handleConfirm} className="h-[34px]">
Confirm Delivery Time
</Button>
<DrawerClose render={<Button variant="secondary" />}>
Cancel
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
The drawer component uses Base UI instead of Vaul. If you installed the previous version, see the migration guide.
pnpm dlx shadcn@latest add https://herocn.dev/r/drawer.jsonAdd the following to your global styles. On iOS Safari, the drawer overlay is absolutely positioned and requires a positioned body to cover the viewport after the page is scrolled. See the Base UI docs for details.
body {
position: relative;
}import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"<Drawer swipeDirection="right">
<DrawerTrigger render={<Button variant="outline" />}>Open</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Are you absolutely sure?</DrawerTitle>
<DrawerDescription>This action cannot be undone.</DrawerDescription>
</DrawerHeader>
<div className="p-4">{/* Content here */}</div>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose render={<Button variant="outline" />}>Cancel</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>Use the following composition to build a Drawer:
Drawer
├── DrawerTrigger
└── DrawerContent
├── DrawerHeader
│ ├── DrawerTitle
│ └── DrawerDescription
└── DrawerFooterDrawerContent composes the portal, overlay, and popup from Base UI. For lower-level control, DrawerPortal and DrawerOverlay are also exported.
Use the swipeDirection prop on Drawer to set the edge the drawer slides from. Available options are up, right, down, and left.
"use client";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
const sides = [
{
direction: "right",
label: "Right",
},
{
direction: "left",
label: "Left",
},
{
direction: "down",
label: "Bottom",
},
{
direction: "up",
label: "Top",
},
] as const;
export function DrawerDirection() {
return (
<div className="flex flex-wrap gap-2">
{sides.map((side) => (
<Drawer key={side.direction} swipeDirection={side.direction}>
<DrawerTrigger render={<Button variant="secondary" />}>
{side.label}
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>{side.label} drawer</DrawerTitle>
<DrawerDescription>
Slides in from the {side.label.toLowerCase()} edge of the
screen.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="size-full rounded-2xl bg-muted group-data-[swipe-axis=x]/drawer-content:size-full group-data-[swipe-axis=y]/drawer-content:h-64 group-data-[swipe-axis=y]/drawer-content:w-full" />
</div>
<DrawerFooter>
<DrawerClose render={<Button className="secondary" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
))}
</div>
);
}
Use overlayVariant on DrawerContent to choose an opaque dim (opaque), a blurred backdrop (blur), or transparent overlay (transparent).
"use client";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export function DrawerOverlayVariant() {
return (
<div className="flex flex-wrap gap-2">
<Drawer>
<DrawerTrigger render={<Button variant="secondary" />}>
Opaque
</DrawerTrigger>
<DrawerContent overlayVariant="opaque">
<DrawerHeader>
<DrawerTitle>Opaque overlay</DrawerTitle>
<DrawerDescription>
Default dimmed backdrop without extra blur on the page behind the
drawer.
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose render={<Button variant="secondary" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
<Drawer>
<DrawerTrigger render={<Button variant="secondary" />}>
Blur
</DrawerTrigger>
<DrawerContent overlayVariant="blur">
<DrawerHeader>
<DrawerTitle>Blurred overlay</DrawerTitle>
<DrawerDescription>
Backdrop blur so content behind the drawer is visibly softened.
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose render={<Button variant="secondary" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
<Drawer>
<DrawerTrigger render={<Button variant="secondary" />}>
Transparent
</DrawerTrigger>
<DrawerContent overlayVariant="transparent">
<DrawerHeader>
<DrawerTitle>Transparent overlay</DrawerTitle>
<DrawerDescription>
No dimmed backdrop — the page stays fully visible behind the
drawer surface.
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose render={<Button variant="secondary" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
</div>
);
}
A vertical drawer sizes itself to its content and is capped at calc(100dvh - 6rem) by default. A side drawer spans 75% of the viewport width, or 24rem on larger screens.
Override the height of a vertical drawer with h-* and max-h-* utilities on DrawerContent.
<DrawerContent className="h-[50vh]">Override the width of a side drawer with w-* and max-w-* utilities on DrawerContent.
<DrawerContent className="w-96">When the same drawer renders in multiple directions, scope an override to one axis with the data-[swipe-axis=*] variants.
<DrawerContent className="data-[swipe-axis=y]:max-h-[50vh] data-[swipe-axis=x]:w-96">To make a region of the drawer scroll, make the scroll container a flex item. Avoid h-full, which does not resolve inside a content-sized drawer.
"use client";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Surface } from "@/components/ui/surface";
const items = Array.from({ length: 12 }, (_, i) => ({
title: `Settings item ${i + 1}`,
description: "Long content scrolls while the header and footer stay in view.",
}));
export function DrawerScrollableContent() {
return (
<Drawer>
<DrawerTrigger render={<Button variant="secondary" />}>
Open Scrollable Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Scrollable content</DrawerTitle>
<DrawerDescription>
Make the scroll container a flex item so it fills the drawer.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 overflow-y-auto p-4">
<ul className="grid gap-2">
{items.map((item) => (
<Surface
key={item.title}
variant="secondary"
className="rounded-3xl p-6 font-medium text-sm"
>
{item.title}
<p className="mt-1 font-normal text-muted-foreground">
{item.description}
</p>
</Surface>
))}
</ul>
</div>
<DrawerFooter>
<Button>Save</Button>
<DrawerClose render={<Button variant="secondary" />}>
Cancel
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
Use showSwipeHandle on Drawer to render a visible swipe handle.
"use client";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export function DrawerSwipeHandle() {
return (
<Drawer showSwipeHandle>
<DrawerTrigger render={<Button variant="secondary" />}>
Open Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Swipe handle</DrawerTitle>
<DrawerDescription>
Drawer with a visible swipe handle you can drag to close.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="rounded-2xl bg-muted group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:h-80 group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<DrawerClose render={<Button />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
Open drawers from inside another drawer. Parent drawers stay mounted and stack behind the frontmost drawer.
"use client";
import { useIsMobile } from "@/hooks/use-mobile";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export function DrawerNested() {
const isMobile = useIsMobile();
const swipeDirection = isMobile ? "down" : "right";
return (
<Drawer showSwipeHandle={isMobile} swipeDirection={swipeDirection}>
<DrawerTrigger render={<Button variant="secondary" />}>
Open Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Drawer</DrawerTitle>
<DrawerDescription>
Open another drawer from the same direction.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="bg-muted group-data-[swipe-axis=y]/drawer-popup:aspect-video group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<Drawer showSwipeHandle={isMobile} swipeDirection={swipeDirection}>
<DrawerTrigger render={<Button />}>
Open Nested Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Nested Drawer</DrawerTitle>
<DrawerDescription>
The parent drawer stays mounted behind this one.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="bg-muted group-data-[swipe-axis=y]/drawer-popup:aspect-video group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<Drawer
showSwipeHandle={isMobile}
swipeDirection={swipeDirection}
>
<DrawerTrigger render={<Button />}>
Open Third Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Third Drawer</DrawerTitle>
<DrawerDescription>
Two drawers are stacked behind this one.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="bg-muted group-data-[swipe-axis=y]/drawer-popup:aspect-video group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<Drawer
showSwipeHandle={isMobile}
swipeDirection={swipeDirection}
>
<DrawerTrigger render={<Button />}>
Open Fourth Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Fourth Drawer</DrawerTitle>
<DrawerDescription>
This is the frontmost drawer in the stack.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="bg-muted group-data-[swipe-axis=y]/drawer-popup:aspect-video group-data-[swipe-axis=x]/drawer-popup:size-full group-data-[swipe-axis=y]/drawer-popup:w-full" />
</div>
<DrawerFooter>
<DrawerClose
render={<Button variant="secondary" />}
>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
<DrawerClose render={<Button variant="secondary" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
<DrawerClose render={<Button variant="secondary" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
<DrawerClose render={<Button variant="secondary" />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
Set modal={false} to allow interaction with the rest of the page while the drawer is open. Combine with disablePointerDismissal to prevent the drawer from closing on outside presses. Use modal="trap-focus" to keep focus inside the drawer while leaving scroll and pointer interaction unrestricted.
"use client";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export function DrawerNonModal() {
return (
<Drawer modal={false} disablePointerDismissal swipeDirection="right">
<DrawerTrigger render={<Button variant="secondary" />}>
Non Modal
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Non modal drawer</DrawerTitle>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="rounded-2xl bg-muted group-data-[swipe-axis=x]/drawer-content:size-full group-data-[swipe-axis=y]/drawer-content:h-80 group-data-[swipe-axis=y]/drawer-content:w-full" />
</div>
<DrawerFooter>
<DrawerClose render={<Button />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
Use snapPoints to snap a drawer to preset heights. Numbers between 0 and 1 represent fractions of the viewport. Numbers greater than 1 are treated as pixel values. String values support px and rem units. Snap points apply to vertical drawers.
"use client";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
const SNAP_POINTS = ["31rem", 1];
export function DrawerSnapPoints() {
return (
<Drawer snapPoints={SNAP_POINTS} showSwipeHandle>
<DrawerTrigger render={<Button variant="secondary" />}>
Open Snap Drawer
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Snap points</DrawerTitle>
<DrawerDescription>
Drag the drawer to snap between a compact peek and a near
full-height view.
</DrawerDescription>
</DrawerHeader>
<div className="flex-1 p-4">
<div className="rounded-2xl bg-muted group-data-[swipe-axis=x]/drawer-content:size-full group-data-[swipe-axis=y]/drawer-content:h-80 group-data-[swipe-axis=y]/drawer-content:w-full" />
</div>
<DrawerFooter>
<DrawerClose render={<Button />}>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
Combine the Dialog and Drawer components to render a Dialog on desktop and a Drawer on mobile.
"use client";
import { cn } from "cn";
import * as React from "react";
import { useIsMobile } from "@/hooks/use-mobile";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function DrawerResponsiveDialog() {
const [open, setOpen] = React.useState(false);
const isMobile = useIsMobile();
if (isMobile) {
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger render={<Button variant="secondary" />}>
Edit Profile
</DrawerTrigger>
<DrawerContent className="p-4">
<DrawerHeader className="text-left">
<DrawerTitle>Edit profile</DrawerTitle>
<DrawerDescription>
Make changes to your profile here. Click save when you're
done.
</DrawerDescription>
</DrawerHeader>
<ProfileForm />
</DrawerContent>
</Drawer>
);
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button variant="secondary" />}>
Edit Profile
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<ProfileForm />
</DialogContent>
</Dialog>
);
}
function ProfileForm({ className }: React.ComponentProps<"form">) {
return (
<form
onSubmit={(e) => e.preventDefault()}
className={cn("grid items-start gap-6", className)}
>
<div className="grid gap-3">
<Label htmlFor="responsive-email">Email</Label>
<Input
variant="secondary"
type="email"
id="responsive-email"
defaultValue="[email protected]"
/>
</div>
<div className="grid gap-3">
<Label htmlFor="responsive-username">Username</Label>
<Input
variant="secondary"
id="responsive-username"
defaultValue="@0xMaqed"
/>
</div>
<Button className="w-fit justify-self-end" type="submit">
Save changes
</Button>
</form>
);
}
The drawer now uses Base UI instead of Vaul. If you installed the previous drawer, update your usage to the Base UI API.
- npm install vaul
+ npm install @base-ui/reactdirection with swipeDirection.Use down instead of bottom, and up instead of top. left and right stay the same.
- <Drawer direction="bottom">
+ <Drawer swipeDirection="down">asChild with render.For DrawerTrigger, pass the trigger element to the render prop.
- <DrawerTrigger asChild>
- <Button variant="outline">Open</Button>
- </DrawerTrigger>
+ <DrawerTrigger render={<Button variant="outline" />}>
+ Open
+ </DrawerTrigger>For DrawerClose, pass the close element to the render prop.
- <DrawerClose asChild>
- <Button variant="outline">Cancel</Button>
- </DrawerClose>
+ <DrawerClose render={<Button variant="outline" />}>
+ Cancel
+ </DrawerClose>If you use snap points, rename the controlled snap point props and the sequential snap point prop.
<Drawer
snapPoints={[0.25, 0.5, 1]}
- activeSnapPoint={snapPoint}
- setActiveSnapPoint={setSnapPoint}
- snapToSequentialPoint
+ snapPoint={snapPoint}
+ onSnapPointChange={setSnapPoint}
+ snapToSequentialPoints
>- <Drawer onAnimationEnd={(open) => setDone(open)}>
+ <Drawer onOpenChangeComplete={(open) => setDone(open)}>- <DrawerContent onOpenAutoFocus={(event) => event.preventDefault()}>
+ <DrawerContent initialFocus={false}>Vaul props like handleOnly, repositionInputs, and shouldScaleBackground do not have one-to-one replacements. Use Base UI props such as disablePointerDismissal, modal, snapPoints, or a controlled open state for the behavior you need.
- <Drawer dismissible={false}>
+ <Drawer disablePointerDismissal>Replace Vaul's data-vaul-drawer-direction selectors with Base UI's data-swipe-direction selectors.
- <DrawerContent className="data-[vaul-drawer-direction=bottom]:max-h-[50vh]">
+ <DrawerContent className="data-[swipe-direction=down]:max-h-[50vh]">To enable RTL support in herocn, see the RTL configuration guide.
"use client";
import * as React from "react";
import {
type Translations,
useTranslation,
} from "@/components/language-selector";
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-mobile";
const deliveryTimes = [
{
value: "asap",
id: "delivery-rtl-asap",
en: {
label: "Standard delivery",
description: "25–35 min · Driver assigned now",
badge: "Fastest",
},
ar: {
label: "توصيل عادي",
description: "25–35 دقيقة · تم تعيين السائق الآن",
badge: "الأسرع",
},
he: {
label: "משלוח רגיל",
description: "25–35 דקות · הנהג שובץ כעת",
badge: "המהיר ביותר",
},
},
{
value: "5-00",
id: "delivery-rtl-5-00",
en: { label: "5:00 PM – 5:15 PM", description: "Prep starts at 4:45 PM" },
ar: { label: "5:00 م – 5:15 م", description: "يبدأ التحضير الساعة 4:45 م" },
he: { label: "17:00 – 17:15", description: "ההכנה מתחילה ב-16:45" },
},
{
value: "5-30",
id: "delivery-rtl-5-30",
en: {
label: "5:30 PM – 5:45 PM",
description: "Good if you're heading home",
},
ar: {
label: "5:30 م – 5:45 م",
description: "مناسب إذا كنت عائد إلى المنزل",
},
he: { label: "17:30 – 17:45", description: "מתאים אם אתה בדרך הביתה" },
},
{
value: "6-00",
id: "delivery-rtl-6-00",
en: {
label: "6:00 PM – 6:15 PM",
description: "Most popular · High demand",
},
ar: { label: "6:00 م – 6:15 م", description: "الأكثر شعبية · الطلب مرتفع" },
he: { label: "18:00 – 18:15", description: "הכי פופולרי · ביקוש גבוה" },
},
{
value: "6-30",
id: "delivery-rtl-6-30",
en: {
label: "6:30 PM – 6:45 PM",
description: "Last slot before kitchen closes",
},
ar: { label: "6:30 م – 6:45 م", description: "آخر موعد قبل إغلاق المطبخ" },
he: {
label: "18:30 – 18:45",
description: "ה SLOT האחרון לפני סגירת המטבח",
},
},
];
const translations: Translations = {
en: {
dir: "ltr",
values: {
open: "Open Drawer",
title: "Pick a delivery time",
description: "We'll prepare your order as soon as possible.",
confirm: "Confirm Delivery Time",
cancel: "Cancel",
toastTitle: "Delivery time confirmed",
},
},
ar: {
dir: "rtl",
values: {
open: "فتح الدرج",
title: "اختر وقت التوصيل",
description: "سنحضر طلبك في أسرع وقت ممكن.",
confirm: "تأكيد وقت التوصيل",
cancel: "إلغاء",
toastTitle: "تم تأكيد وقت التوصيل",
},
},
he: {
dir: "rtl",
values: {
open: "פתח מגירה",
title: "בחר זמן משלוח",
description: "נכין את ההזמנה שלך בהקדם האפשרי.",
confirm: "אשר זמן משלוח",
cancel: "ביטול",
toastTitle: "זמן המשלוח אושר",
},
},
};
export function DrawerRtl() {
const { dir, t, language } = useTranslation(translations, "ar");
const [open, setOpen] = React.useState(false);
const isMobile = useIsMobile();
const [deliveryTime, setDeliveryTime] = React.useState("asap");
function handleConfirm() {
const selected = deliveryTimes.find((time) => time.value === deliveryTime);
if (!selected) {
return;
}
setOpen(false);
toast.add({
title: t.toastTitle,
description: selected[language]?.label ?? selected.en.label,
});
}
return (
<Drawer
open={open}
onOpenChange={setOpen}
swipeDirection={isMobile ? "down" : dir === "rtl" ? "left" : "right"}
data-lang={dir === "rtl" ? language : undefined}
>
<DrawerTrigger
data-lang={language}
render={<Button variant="secondary" />}
>
{t.open}
</DrawerTrigger>
<DrawerContent dir={dir}>
<DrawerHeader>
<DrawerTitle>{t.title}</DrawerTitle>
<DrawerDescription>{t.description}</DrawerDescription>
</DrawerHeader>
<div className="scroll-fade flex-1 overflow-y-auto p-4">
<RadioGroup
variant="secondary"
value={deliveryTime}
onValueChange={setDeliveryTime}
className="gap-2"
>
{deliveryTimes.map((time) => (
<FieldLabel key={time.value} htmlFor={time.id}>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle className="flex items-center gap-2">
{time[language]?.label ?? time.en.label}
{time[language].badge ? (
<Badge variant="primary">{time[language].badge}</Badge>
) : null}
</FieldTitle>
<FieldDescription>
{time[language]?.description ?? time.en.description}
</FieldDescription>
</FieldContent>
<RadioGroupItem value={time.value} id={time.id} />
</Field>
</FieldLabel>
))}
</RadioGroup>
</div>
<DrawerFooter>
<Button onClick={handleConfirm} className="h-[34px]">
{t.confirm}
</Button>
<DrawerClose render={<Button variant="secondary" />}>
{t.cancel}
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
See the Base UI Drawer documentation for more information.