Responsive Dialog
A dialog component that adapts to screen size - side dialog on desktop, drawer on mobile.
npx shadcn@latest add https://shuip.plvo.dev/r/responsive-dialog.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/responsive-dialog.json
bun x shadcn@latest add https://shuip.plvo.dev/r/responsive-dialog.json
'use client';import * as React from 'react';import {Drawer,DrawerClose,DrawerContent,DrawerDescription,DrawerFooter,DrawerHeader,DrawerTitle,DrawerTrigger,} from '@/components/ui/drawer';import {SideDialog,SideDialogClose,SideDialogContent,SideDialogDescription,SideDialogFooter,SideDialogHeader,SideDialogTitle,SideDialogTrigger,} from '@/components/ui/shuip/side-dialog';type ResponsiveDialogPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'left' | 'right';type ResponsiveDialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';type ResponsiveDialogBreakpoint = 'sm' | 'md' | 'lg' | 'xl' | number;export interface ResponsiveDialogProps {open?: boolean;onOpenChange?: (open: boolean) => void;position?: ResponsiveDialogPosition;size?: ResponsiveDialogSize;breakpoint?: ResponsiveDialogBreakpoint;children?: React.ReactNode;}export interface ResponsiveDialogTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {asChild?: boolean;children: React.ReactNode;}export interface ResponsiveDialogContentProps extends React.HTMLAttributes<HTMLDivElement> {showCloseButton?: boolean;children: React.ReactNode;}export interface ResponsiveDialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {children: React.ReactNode;}export interface ResponsiveDialogTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {children: React.ReactNode;}export interface ResponsiveDialogDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {children: React.ReactNode;}export interface ResponsiveDialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {children: React.ReactNode;}export interface ResponsiveDialogCloseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {asChild?: boolean;children?: React.ReactNode;}// Helper function to convert breakpoint to pixelsconst getBreakpointValue = (breakpoint: ResponsiveDialogBreakpoint): number => {if (typeof breakpoint === 'number') {return breakpoint;}const breakpoints = {sm: 640,md: 768,lg: 1024,xl: 1280,};return breakpoints[breakpoint];};// Custom hook for responsive breakpoint detectionconst useCustomBreakpoint = (breakpoint: ResponsiveDialogBreakpoint) => {const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);React.useEffect(() => {const breakpointValue = getBreakpointValue(breakpoint);const mql = window.matchMedia(`(max-width: ${breakpointValue - 1}px)`);const onChange = () => {setIsMobile(window.innerWidth < breakpointValue);};mql.addEventListener('change', onChange);setIsMobile(window.innerWidth < breakpointValue);return () => mql.removeEventListener('change', onChange);}, [breakpoint]);return !!isMobile;};// Context for managing responsive stateconst ResponsiveDialogContext = React.createContext<{isMobile: boolean;}>({isMobile: false,});export function ResponsiveDialog({open,onOpenChange,position = 'bottom-right',size = 'sm',breakpoint = 'md',children,}: ResponsiveDialogProps) {const isMobile = useCustomBreakpoint(breakpoint);return (<ResponsiveDialogContext.Provider value={{ isMobile }}>{isMobile ? (<Drawer open={open} onOpenChange={onOpenChange}>{children}</Drawer>) : (<SideDialog open={open} onOpenChange={onOpenChange} position={position} size={size}>{children}</SideDialog>)}</ResponsiveDialogContext.Provider>);}export function ResponsiveDialogTrigger({ asChild, children, ...props }: ResponsiveDialogTriggerProps) {const { isMobile } = React.useContext(ResponsiveDialogContext);if (isMobile) {return (<DrawerTrigger asChild={asChild} {...props}>{children}</DrawerTrigger>);}return (<SideDialogTrigger asChild={asChild} {...props}>{children}</SideDialogTrigger>);}export function ResponsiveDialogContent({showCloseButton = true,className,children,...props}: ResponsiveDialogContentProps) {const { isMobile } = React.useContext(ResponsiveDialogContext);if (isMobile) {return (<DrawerContent className={className} {...props}>{children}</DrawerContent>);}return (<SideDialogContent showCloseButton={showCloseButton} className={className} {...props}>{children}</SideDialogContent>);}export function ResponsiveDialogHeader({ className, children, ...props }: ResponsiveDialogHeaderProps) {const { isMobile } = React.useContext(ResponsiveDialogContext);if (isMobile) {return (<DrawerHeader className={className} {...props}>{children}</DrawerHeader>);}return (<SideDialogHeader className={className} {...props}>{children}</SideDialogHeader>);}export function ResponsiveDialogTitle({ className, children, ...props }: ResponsiveDialogTitleProps) {const { isMobile } = React.useContext(ResponsiveDialogContext);if (isMobile) {return (<DrawerTitle className={className} {...props}>{children}</DrawerTitle>);}return (<SideDialogTitle className={className} {...props}>{children}</SideDialogTitle>);}export function ResponsiveDialogDescription({ className, children, ...props }: ResponsiveDialogDescriptionProps) {const { isMobile } = React.useContext(ResponsiveDialogContext);if (isMobile) {return (<DrawerDescription className={className} {...props}>{children}</DrawerDescription>);}return (<SideDialogDescription className={className} {...props}>{children}</SideDialogDescription>);}export function ResponsiveDialogFooter({ className, children, ...props }: ResponsiveDialogFooterProps) {const { isMobile } = React.useContext(ResponsiveDialogContext);if (isMobile) {return (<DrawerFooter className={className} {...props}>{children}</DrawerFooter>);}return (<SideDialogFooter className={className} {...props}>{children}</SideDialogFooter>);}export function ResponsiveDialogClose({ asChild, children, ...props }: ResponsiveDialogCloseProps) {const { isMobile } = React.useContext(ResponsiveDialogContext);if (isMobile) {return (<DrawerClose asChild={asChild} {...props}>{children}</DrawerClose>);}return (<SideDialogClose asChild={asChild} {...props}>{children}</SideDialogClose>);}
Loading...
About
ResponsiveDialog is a smart dialog component that automatically adapts to the user's screen size:
- Desktop (≥1024px): Renders as a
SideDialogwith flexible positioning - Mobile (<1024px): Renders as a
Drawerfor better mobile UX
Unlike traditional dialogs that force the same experience across all devices, ResponsiveDialog provides the optimal interface for each screen size while maintaining a consistent API.
Built-in features
- Automatic responsive behavior: Switches between side dialog and drawer based on screen size
- 6 positions (desktop): Choose from
top-left,top-right,bottom-left,bottom-right,left,right - Flexible sizes (desktop): Predefined sizes (
xs,sm,md,lg,xl,2xl) - Mobile-optimized: Uses native drawer with swipe gestures on mobile
- Lightweight: Combines existing components without additional overhead
- Accessible: Inherits accessibility features from both SideDialog and Drawer
Usage
Basic responsive dialog
<ResponsiveDialog>
<ResponsiveDialogTrigger asChild>
<Button>Open Dialog</Button>
</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Settings</ResponsiveDialogTitle>
<ResponsiveDialogDescription>
Manage your preferences and account settings.
</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
<ResponsiveDialogFooter>
<ResponsiveDialogClose asChild>
<Button variant="outline">Cancel</Button>
</ResponsiveDialogClose>
<Button>Save</Button>
</ResponsiveDialogFooter>
</ResponsiveDialogContent>
</ResponsiveDialog>Different positions (desktop only)
Desktop positions are ignored on mobile where the drawer behavior takes precedence:
// Notification from bottom-right
<ResponsiveDialog position="bottom-right">
<ResponsiveDialogTrigger asChild>
<Button>Show Notification</Button>
</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Success!</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Your changes have been saved.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>
// User menu from top-right
<ResponsiveDialog position="top-right" size="sm">
<ResponsiveDialogTrigger asChild>
<Button variant="ghost" size="icon">
<User className="size-4" />
</Button>
</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Account</ResponsiveDialogTitle>
</ResponsiveDialogHeader>
<div className="space-y-2">
<Button variant="ghost" className="w-full justify-start">
Profile Settings
</Button>
<Button variant="ghost" className="w-full justify-start">
Sign out
</Button>
</div>
</ResponsiveDialogContent>
</ResponsiveDialog>Custom breakpoints
Control when the dialog switches between desktop and mobile modes:
// Use predefined breakpoints
<ResponsiveDialog breakpoint="sm"> {/* Switches at 640px */}
<ResponsiveDialogTrigger asChild>
<Button>Small Breakpoint</Button>
</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Mobile at 640px</ResponsiveDialogTitle>
<ResponsiveDialogDescription>
This dialog becomes a drawer on screens smaller than 640px.
</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>
// Use custom pixel values
<ResponsiveDialog breakpoint={900}>
<ResponsiveDialogTrigger asChild>
<Button>Custom Breakpoint</Button>
</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Custom at 900px</ResponsiveDialogTitle>
<ResponsiveDialogDescription>
Switches to mobile mode at exactly 900px and below.
</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>Different sizes (desktop only)
Size props affect the desktop side dialog appearance:
// Small dialog for quick actions
<ResponsiveDialog size="xs">
<ResponsiveDialogTrigger asChild>
<Button variant="outline">Quick Action</Button>
</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Confirm</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Are you sure?</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>
// Large dialog for complex content
<ResponsiveDialog position="left" size="lg">
<ResponsiveDialogTrigger asChild>
<Button variant="outline">Navigation Menu</Button>
</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Navigation</ResponsiveDialogTitle>
</ResponsiveDialogHeader>
<nav className="space-y-2">
<Link href="/dashboard">Dashboard</Link>
<Link href="/projects">Projects</Link>
<Link href="/settings">Settings</Link>
</nav>
</ResponsiveDialogContent>
</ResponsiveDialog>Common use cases
Mobile-first navigation
<ResponsiveDialog position="left" size="lg">
<ResponsiveDialogTrigger asChild>
<Button variant="outline" size="icon">
<Menu className="size-4" />
</Button>
</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Menu</ResponsiveDialogTitle>
</ResponsiveDialogHeader>
<nav className="space-y-3">
<Link href="/home" className="block p-2 hover:bg-muted rounded">
Home
</Link>
<Link href="/about" className="block p-2 hover:bg-muted rounded">
About
</Link>
<Link href="/contact" className="block p-2 hover:bg-muted rounded">
Contact
</Link>
</nav>
</ResponsiveDialogContent>
</ResponsiveDialog>Examples
Default
Loading...
'use client';import { zodResolver } from '@hookform/resolvers/zod';import React from 'react';import { useForm } from 'react-hook-form';import z from 'zod';import {ResponsiveDialog,ResponsiveDialogClose,ResponsiveDialogContent,ResponsiveDialogDescription,ResponsiveDialogFooter,ResponsiveDialogHeader,ResponsiveDialogTitle,ResponsiveDialogTrigger,} from '@/components/block/shuip/responsive-dialog';import { Button } from '@/components/ui/button';import { Form } from '@/components/ui/form';import { InputField } from '@/components/ui/shuip/react-hook-form/input-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';export default function ResponsiveDialogExample() {return (<div className='space-y-4'><h3 className='text-lg font-semibold'>Basic Responsive Dialog</h3><div className='flex flex-wrap gap-2'><ResponsiveDialog><ResponsiveDialogTrigger asChild><Button variant='outline'>Open Dialog</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Responsive Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>This dialog adapts to your screen size. On desktop, it's a side dialog. On mobile, it's a drawer.</ResponsiveDialogDescription></ResponsiveDialogHeader><div className='p-4'><p className='text-sm text-muted-foreground'>Try resizing your browser window or switching to mobile view to see the responsive behavior.</p></div><ResponsiveDialogFooter><ResponsiveDialogClose asChild><Button variant='outline'>Close</Button></ResponsiveDialogClose><Button>Continue</Button></ResponsiveDialogFooter></ResponsiveDialogContent></ResponsiveDialog></div><div className='space-y-2'><h3 className='text-lg font-semibold'>Different Positions (Desktop only)</h3><div className='flex flex-wrap gap-2'><ResponsiveDialog position='top-left'><ResponsiveDialogTrigger asChild><Button variant='outline'>Top Left</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Top Left Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>Positioned at top left on desktop.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog><ResponsiveDialog position='top-right'><ResponsiveDialogTrigger asChild><Button variant='outline'>Top Right</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Top Right Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>Positioned at top right on desktop.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog><ResponsiveDialog position='bottom-left'><ResponsiveDialogTrigger asChild><Button variant='outline'>Bottom Left</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Bottom Left Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>Positioned at bottom left on desktop.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog><ResponsiveDialog position='bottom-right'><ResponsiveDialogTrigger asChild><Button variant='outline'>Bottom Right</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Bottom Right Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>Positioned at bottom right on desktop.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog><ResponsiveDialog position='left'><ResponsiveDialogTrigger asChild><Button variant='outline'>Left</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Left Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>Positioned at left side on desktop.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog><ResponsiveDialog position='right'><ResponsiveDialogTrigger asChild><Button variant='outline'>Right</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Right Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>Positioned at right side on desktop.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog></div></div><div className='space-y-2'><h3 className='text-lg font-semibold'>Different Sizes (Desktop only)</h3><div className='flex flex-wrap gap-2'><ResponsiveDialog size='xs'><ResponsiveDialogTrigger asChild><Button variant='outline'>Extra Small</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>XS Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>Extra small dialog size.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog><ResponsiveDialog size='sm'><ResponsiveDialogTrigger asChild><Button variant='outline'>Small</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Small Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>Small dialog size (default).</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog><ResponsiveDialog size='md'><ResponsiveDialogTrigger asChild><Button variant='outline'>Medium</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Medium Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>Medium dialog size.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog><ResponsiveDialog size='lg'><ResponsiveDialogTrigger asChild><Button variant='outline'>Large</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Large Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>Large dialog size for more content.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog><ResponsiveDialog size='xl'><ResponsiveDialogTrigger asChild><Button variant='outline'>Extra Large</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>XL Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>Extra large dialog size.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog><ResponsiveDialog size='2xl'><ResponsiveDialogTrigger asChild><Button variant='outline'>2X Large</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>2XL Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>2X large dialog size for extensive content.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog></div></div><div className='space-y-2'><h3 className='text-lg font-semibold'>Custom Breakpoints</h3><div className='flex flex-wrap gap-2'><ResponsiveDialog breakpoint='sm'><ResponsiveDialogTrigger asChild><Button variant='outline'>Breakpoint SM (640px)</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Small Breakpoint</ResponsiveDialogTitle><ResponsiveDialogDescription>This dialog switches to mobile mode at 640px and below.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog><ResponsiveDialog breakpoint='md'><ResponsiveDialogTrigger asChild><Button variant='outline'>Breakpoint MD (768px)</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Medium Breakpoint</ResponsiveDialogTitle><ResponsiveDialogDescription>This dialog switches to mobile mode at 768px and below (default).</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog><ResponsiveDialog breakpoint='lg'><ResponsiveDialogTrigger asChild><Button variant='outline'>Breakpoint LG (1024px)</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Large Breakpoint</ResponsiveDialogTitle><ResponsiveDialogDescription>This dialog switches to mobile mode at 1024px and below.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog><ResponsiveDialog breakpoint={900}><ResponsiveDialogTrigger asChild><Button variant='outline'>Custom 900px</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><ResponsiveDialogHeader><ResponsiveDialogTitle>Custom Breakpoint</ResponsiveDialogTitle><ResponsiveDialogDescription>This dialog switches to mobile mode at exactly 900px and below.</ResponsiveDialogDescription></ResponsiveDialogHeader></ResponsiveDialogContent></ResponsiveDialog></div></div><h3 className='text-lg font-semibold'>Form Example</h3><ResponsiveDialogFormExample /></div>);}const formSchema = z.object({name: z.string().min(1),email: z.string().email(),});function ResponsiveDialogFormExample() {const [open, setOpen] = React.useState(false);const form = useForm({resolver: zodResolver(formSchema),});const onSubmit = async (data: z.infer<typeof formSchema>) => {alert(JSON.stringify(data));setOpen(false);form.reset();};return (<ResponsiveDialog open={open} onOpenChange={setOpen}><ResponsiveDialogTrigger asChild><Button variant='outline'>Open Dialog</Button></ResponsiveDialogTrigger><ResponsiveDialogContent><Form {...form}><form onSubmit={form.handleSubmit(onSubmit)}><ResponsiveDialogHeader><ResponsiveDialogTitle>Responsive Dialog</ResponsiveDialogTitle><ResponsiveDialogDescription>This dialog adapts to your screen size. On desktop, it's a side dialog. On mobile, it's a drawer.</ResponsiveDialogDescription></ResponsiveDialogHeader><div className='space-y-4 px-4'><InputField register={form.register('name')} name='name' label='Name' /><InputField register={form.register('email')} name='email' label='Email' /></div><ResponsiveDialogFooter><ResponsiveDialogClose asChild><Button variant='outline'>Close</Button></ResponsiveDialogClose><SubmitButton className='max-md:w-full'>Submit</SubmitButton></ResponsiveDialogFooter></form></Form></ResponsiveDialogContent></ResponsiveDialog>);}
Props
Prop
Type