Responsive Dialog
A modal that adapts to the viewport — a centered Dialog on desktop, a bottom Drawer on mobile, from one shared set of components so you don't maintain two versions of the same form.
Last updated August 8, 2026
CreditsModifiedPublished
Copied from shadcn/ui.
- Not a shadcn/ui component — this composition (switch between Dialog and Drawer by viewport width, behind one shared API) is original to this registry, built on top of this registry's own Dialog and Drawer
- ResponsiveDialogDesktopOnly hides Cancel/انصراف-style actions on mobile, where the drawer is already dismissed by swiping down, so a redundant Cancel button never has to be built twice
Overview
Resize the browser window to see the preview switch between a Dialog and a Drawer.
"use client"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
ResponsiveDialog,
ResponsiveDialogClose,
ResponsiveDialogDescription,
ResponsiveDialogDesktopOnly,
ResponsiveDialogFooter,
ResponsiveDialogHeader,
ResponsiveDialogPanel,
ResponsiveDialogPopup,
ResponsiveDialogTitle,
ResponsiveDialogTrigger,
} from "@/components/ui/responsive-dialog"
export function ResponsiveDialogDemoExample() {
return (
<ResponsiveDialog>
<ResponsiveDialogTrigger
render={<Button variant="outline">Edit profile</Button>}
/>
<ResponsiveDialogPopup>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Edit profile</ResponsiveDialogTitle>
<ResponsiveDialogDescription>
A Dialog on desktop, a bottom Drawer on mobile — resize the window
to see it switch.
</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
<ResponsiveDialogPanel className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="responsive-dialog-demo-name">Name</Label>
<Input
id="responsive-dialog-demo-name"
defaultValue="Sadegh Alavi"
/>
</div>
</ResponsiveDialogPanel>
<ResponsiveDialogFooter>
<Button type="submit">Save changes</Button>
<ResponsiveDialogDesktopOnly>
<ResponsiveDialogClose
render={<Button variant="outline">Cancel</Button>}
/>
</ResponsiveDialogDesktopOnly>
</ResponsiveDialogFooter>
</ResponsiveDialogPopup>
</ResponsiveDialog>
)
}No footer
ResponsiveDialogFooter is optional here too — for a purely informational modal, skip it on both the Dialog and Drawer side.
import { BadgeCheckIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
ResponsiveDialog,
ResponsiveDialogDescription,
ResponsiveDialogHeader,
ResponsiveDialogPanel,
ResponsiveDialogPopup,
ResponsiveDialogTitle,
ResponsiveDialogTrigger,
} from "@/components/ui/responsive-dialog"
export function ResponsiveDialogNoFooterExample() {
return (
<ResponsiveDialog>
<ResponsiveDialogTrigger
render={<Button variant="outline">View receipt</Button>}
/>
<ResponsiveDialogPopup>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Payment successful</ResponsiveDialogTitle>
<ResponsiveDialogDescription>
Your subscription is active. A receipt was sent to your email.
</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
<ResponsiveDialogPanel className="flex items-center gap-3">
<BadgeCheckIcon className="size-8 shrink-0 text-primary" />
<p className="text-sm text-muted-foreground">
Nothing left to do here — dismiss with Escape, a tap outside, or (on
mobile) a swipe down.
</p>
</ResponsiveDialogPanel>
</ResponsiveDialogPopup>
</ResponsiveDialog>
)
}Installation
$ npx shadcn@latest add https://ui.persian-labs.ir/r/responsive-dialog.jsonUsage
import {
ResponsiveDialog,
ResponsiveDialogClose,
ResponsiveDialogDescription,
ResponsiveDialogDesktopOnly,
ResponsiveDialogFooter,
ResponsiveDialogHeader,
ResponsiveDialogPanel,
ResponsiveDialogPopup,
ResponsiveDialogTitle,
ResponsiveDialogTrigger,
} from "@/components/ui/responsive-dialog"
export function Example() {
return (
<ResponsiveDialog>
<ResponsiveDialogTrigger render={<Button variant="outline">Open</Button>} />
<ResponsiveDialogPopup>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Edit profile</ResponsiveDialogTitle>
<ResponsiveDialogDescription>
A Dialog on desktop, a Drawer on mobile.
</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
<ResponsiveDialogPanel>{/* form fields */}</ResponsiveDialogPanel>
<ResponsiveDialogFooter>
<Button type="submit">Save changes</Button>
{/* Skipped on mobile — the drawer is dismissed by swiping. */}
<ResponsiveDialogDesktopOnly>
<ResponsiveDialogClose render={<Button variant="outline">Cancel</Button>} />
</ResponsiveDialogDesktopOnly>
</ResponsiveDialogFooter>
</ResponsiveDialogPopup>
</ResponsiveDialog>
)
}RTL
"use client"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
ResponsiveDialog,
ResponsiveDialogClose,
ResponsiveDialogDescription,
ResponsiveDialogDesktopOnly,
ResponsiveDialogFooter,
ResponsiveDialogHeader,
ResponsiveDialogPanel,
ResponsiveDialogPopup,
ResponsiveDialogTitle,
ResponsiveDialogTrigger,
} from "@/components/ui/responsive-dialog"
export function ResponsiveDialogRtlExample() {
return (
<ResponsiveDialog drawerPosition="bottom">
<ResponsiveDialogTrigger
render={<Button variant="outline">ویرایش پروفایل</Button>}
/>
<ResponsiveDialogPopup>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>ویرایش پروفایل</ResponsiveDialogTitle>
<ResponsiveDialogDescription>
در دسکتاپ یک Dialog و در موبایل یک کشوی پایینی نمایش داده میشود.
</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
<ResponsiveDialogPanel className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="responsive-dialog-rtl-name">نام</Label>
<Input id="responsive-dialog-rtl-name" defaultValue="صادق علوی" />
</div>
</ResponsiveDialogPanel>
<ResponsiveDialogFooter>
<Button type="submit">ذخیره تغییرات</Button>
<ResponsiveDialogDesktopOnly>
<ResponsiveDialogClose
render={<Button variant="outline">انصراف</Button>}
/>
</ResponsiveDialogDesktopOnly>
</ResponsiveDialogFooter>
</ResponsiveDialogPopup>
</ResponsiveDialog>
)
}