Responsive Dialog
A dialog component that adapts to screen size - side dialog on desktop, drawer on mobile.
npx shadcn@latest add @shuip/responsive-dialog
pnpm dlx shadcn@latest add @shuip/responsive-dialog
bun x shadcn@latest add @shuip/responsive-dialog
'use client';
import * as React from 'react';
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '@/components/ui/drawer';
import {
SideDialog,
SideDialogBody,
SideDialogClose,
type SideDialogCloseProps,
SideDialogContent,
SideDialogDescription,
SideDialogFooter,
SideDialogHeader,
SideDialogTitle,
SideDialogTrigger,
type SideDialogTriggerProps,
} from '@/components/ui/shuip/side-dialog';
import { cn } from '@/lib/utils';
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 SideDialogTriggerProps {}
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 SideDialogCloseProps {}
export interface ResponsiveDialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
// Helper function to convert breakpoint to pixels
const 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 detection
const 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 state
const ResponsiveDialogContext = React.createContext<{
isMobile: boolean;
}>({
isMobile: false,
});
export function useResponsiveDialog() {
return React.useContext(ResponsiveDialogContext);
}
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} showSwipeHandle>
{children}
</Drawer>
) : (
<SideDialog open={open} onOpenChange={onOpenChange} position={position} size={size}>
{children}
</SideDialog>
)}
</ResponsiveDialogContext.Provider>
);
}
export function ResponsiveDialogTrigger(props: ResponsiveDialogTriggerProps) {
const { isMobile } = React.useContext(ResponsiveDialogContext);
if (isMobile) {
return <DrawerTrigger {...props} />;
}
return <SideDialogTrigger {...props} />;
}
export function ResponsiveDialogContent({
showCloseButton = true,
className,
children,
...props
}: ResponsiveDialogContentProps) {
const { isMobile } = React.useContext(ResponsiveDialogContext);
if (isMobile) {
return (
<DrawerContent {...props}>
<div className={cn('flex flex-col max-h-[75dvh]', className)}>{children}</div>
</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={cn('shrink-0', 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={cn('shrink-0', className)} {...props}>
{children}
</DrawerFooter>
);
}
return (
<SideDialogFooter className={className} {...props}>
{children}
</SideDialogFooter>
);
}
export function ResponsiveDialogBody({ className, children, ...props }: ResponsiveDialogBodyProps) {
const { isMobile } = React.useContext(ResponsiveDialogContext);
if (isMobile) {
return (
<div className={cn('flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-4 py-3', className)} {...props}>
{children}
</div>
);
}
return (
<SideDialogBody className={className} {...props}>
{children}
</SideDialogBody>
);
}
export function ResponsiveDialogClose(props: ResponsiveDialogCloseProps) {
const { isMobile } = React.useContext(ResponsiveDialogContext);
if (isMobile) {
return <DrawerClose {...props} />;
}
return <SideDialogClose {...props} />;
}
The command is the same for Base UI and Radix: the CLI reads the style in your components.json and installs the matching variant.
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) - Scrollable body:
ResponsiveDialogBodykeeps the header and footer anchored while the content scrolls on both desktop and mobile - 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 render={<Button />}>Open Dialog</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Settings</ResponsiveDialogTitle>
<ResponsiveDialogDescription>
Manage your preferences and account settings.
</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
<ResponsiveDialogFooter>
<ResponsiveDialogClose render={<Button variant="outline" />}>Cancel</ResponsiveDialogClose>
<Button>Save</Button>
</ResponsiveDialogFooter>
</ResponsiveDialogContent>
</ResponsiveDialog>With scrollable content
Use ResponsiveDialogBody when the dialog content may overflow. The header and footer stay anchored while the body scrolls — on both desktop and mobile.
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Activity Log</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Recent actions across your workspace.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
<ResponsiveDialogBody>
{/* long list, form, or any overflowing content */}
</ResponsiveDialogBody>
<ResponsiveDialogFooter>
<ResponsiveDialogClose render={<Button variant="outline" />}>Close</ResponsiveDialogClose>
</ResponsiveDialogFooter>
</ResponsiveDialogContent>On mobile, ResponsiveDialogBody automatically adds horizontal padding (px-4 py-3) to match the drawer's layout. On desktop, padding comes from SideDialogContent.
Wrapper elements (e.g.
<form>): Any element wrappingHeader/Body/FooterinsideResponsiveDialogContentmust carryclassName="flex flex-col flex-1 min-h-0"so it participates in the flex layout. Without it the body has no bounded height and cannot scroll.<ResponsiveDialogContent> <form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0"> <ResponsiveDialogHeader>...</ResponsiveDialogHeader> <ResponsiveDialogBody>...</ResponsiveDialogBody> <ResponsiveDialogFooter>...</ResponsiveDialogFooter> </form> </ResponsiveDialogContent>
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 render={<Button />}>Show Notification</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 render={<Button variant="ghost" size="icon" />}>
<User className="size-4" />
</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 render={<Button />}>Small Breakpoint</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 render={<Button />}>Custom Breakpoint</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 render={<Button variant="outline" />}>Quick Action</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 render={<Button variant="outline" />}>Navigation Menu</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 render={<Button variant="outline" size="icon" />}>
<Menu className="size-4" />
</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
'use client';
import { useLens } from '@hookform/lenses';
import { zodResolver } from '@hookform/resolvers/zod';
import React from 'react';
import { useForm } from 'react-hook-form';
import z from 'zod';
import {
ResponsiveDialog,
ResponsiveDialogBody,
ResponsiveDialogClose,
ResponsiveDialogContent,
ResponsiveDialogDescription,
ResponsiveDialogFooter,
ResponsiveDialogHeader,
ResponsiveDialogTitle,
ResponsiveDialogTrigger,
} from '@/components/block/shuip/responsive-dialog';
import { Button } from '@/components/ui/button';
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 render={<Button variant='outline' />}>Open Dialog</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 render={<Button variant='outline' />}>Close</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 render={<Button variant='outline' />}>Top Left</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Top Left Dialog</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Positioned at top left on desktop.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>
<ResponsiveDialog position='top-right'>
<ResponsiveDialogTrigger render={<Button variant='outline' />}>Top Right</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Top Right Dialog</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Positioned at top right on desktop.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>
<ResponsiveDialog position='bottom-left'>
<ResponsiveDialogTrigger render={<Button variant='outline' />}>Bottom Left</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Bottom Left Dialog</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Positioned at bottom left on desktop.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>
<ResponsiveDialog position='bottom-right'>
<ResponsiveDialogTrigger render={<Button variant='outline' />}>Bottom Right</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Bottom Right Dialog</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Positioned at bottom right on desktop.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>
<ResponsiveDialog position='left'>
<ResponsiveDialogTrigger render={<Button variant='outline' />}>Left</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Left Dialog</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Positioned at left side on desktop.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>
<ResponsiveDialog position='right'>
<ResponsiveDialogTrigger render={<Button variant='outline' />}>Right</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 render={<Button variant='outline' />}>Extra Small</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>XS Dialog</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Extra small dialog size.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>
<ResponsiveDialog size='sm'>
<ResponsiveDialogTrigger render={<Button variant='outline' />}>Small</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Small Dialog</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Small dialog size (default).</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>
<ResponsiveDialog size='md'>
<ResponsiveDialogTrigger render={<Button variant='outline' />}>Medium</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Medium Dialog</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Medium dialog size.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>
<ResponsiveDialog size='lg'>
<ResponsiveDialogTrigger render={<Button variant='outline' />}>Large</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Large Dialog</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Large dialog size for more content.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>
<ResponsiveDialog size='xl'>
<ResponsiveDialogTrigger render={<Button variant='outline' />}>Extra Large</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>XL Dialog</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Extra large dialog size.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
</ResponsiveDialogContent>
</ResponsiveDialog>
<ResponsiveDialog size='2xl'>
<ResponsiveDialogTrigger render={<Button variant='outline' />}>2X Large</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 render={<Button variant='outline' />}>
Breakpoint SM (640px)
</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 render={<Button variant='outline' />}>
Breakpoint MD (768px)
</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 render={<Button variant='outline' />}>
Breakpoint LG (1024px)
</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 render={<Button variant='outline' />}>Custom 900px</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({
defaultValues: { name: '', email: '' },
resolver: zodResolver(formSchema),
});
const lens = useLens({ control: form.control });
const onSubmit = async (data: z.infer<typeof formSchema>) => {
alert(JSON.stringify(data));
setOpen(false);
form.reset();
};
return (
<ResponsiveDialog open={open} onOpenChange={setOpen}>
<ResponsiveDialogTrigger render={<Button variant='outline' />}>Open Dialog</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<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>
<ResponsiveDialogBody className='space-y-4'>
<InputField lens={lens.focus('name')} label='Name' />
<InputField lens={lens.focus('email')} label='Email' />
</ResponsiveDialogBody>
<ResponsiveDialogFooter>
<ResponsiveDialogClose render={<Button variant='outline' />}>Close</ResponsiveDialogClose>
<SubmitButton className='max-md:w-full'>Submit</SubmitButton>
</ResponsiveDialogFooter>
</form>
</ResponsiveDialogContent>
</ResponsiveDialog>
);
}
Scrollable
'use client';
import { useLens } from '@hookform/lenses';
import { zodResolver } from '@hookform/resolvers/zod';
import React from 'react';
import { useForm } from 'react-hook-form';
import z from 'zod';
import {
ResponsiveDialog,
ResponsiveDialogBody,
ResponsiveDialogClose,
ResponsiveDialogContent,
ResponsiveDialogDescription,
ResponsiveDialogFooter,
ResponsiveDialogHeader,
ResponsiveDialogTitle,
ResponsiveDialogTrigger,
} from '@/components/block/shuip/responsive-dialog';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { InputField } from '@/components/ui/shuip/react-hook-form/input-field';
import { SelectField } from '@/components/ui/shuip/react-hook-form/select-field';
import { TextareaField } from '@/components/ui/shuip/react-hook-form/textarea-field';
import { SubmitButton } from '@/components/ui/shuip/submit-button';
const ACTIVITY_LOG = [
{ id: 1, user: 'Alice Martin', action: 'Updated billing address', time: '2 minutes ago', type: 'edit' },
{ id: 2, user: 'Bob Chen', action: 'Exported 847 records to CSV', time: '14 minutes ago', type: 'export' },
{ id: 3, user: 'Alice Martin', action: 'Invited carol@example.com', time: '1 hour ago', type: 'invite' },
{ id: 4, user: 'System', action: 'Scheduled backup completed', time: '2 hours ago', type: 'system' },
{ id: 5, user: 'Dave Wilson', action: 'Deleted workspace "Legacy"', time: '3 hours ago', type: 'delete' },
{ id: 6, user: 'Alice Martin', action: 'Changed plan to Pro', time: '5 hours ago', type: 'edit' },
{ id: 7, user: 'Bob Chen', action: 'Created API key "Production"', time: '6 hours ago', type: 'create' },
{ id: 8, user: 'System', action: 'SSL certificate renewed', time: 'Yesterday at 11:42 PM', type: 'system' },
{ id: 9, user: 'Dave Wilson', action: 'Added 3 new team members', time: 'Yesterday at 4:12 PM', type: 'invite' },
{ id: 10, user: 'Carol Lee', action: 'Reset 2FA for bob@example.com', time: 'Yesterday at 2:05 PM', type: 'edit' },
{ id: 11, user: 'Alice Martin', action: 'Enabled SSO for domain acme.com', time: '2 days ago', type: 'edit' },
{ id: 12, user: 'System', action: 'Monthly invoice generated — $429.00', time: '3 days ago', type: 'system' },
{ id: 13, user: 'Bob Chen', action: 'Revoked API key "Staging"', time: '3 days ago', type: 'delete' },
{ id: 14, user: 'Carol Lee', action: 'Updated webhook endpoint', time: '4 days ago', type: 'edit' },
{ id: 15, user: 'Dave Wilson', action: 'Archived project "Q3 Campaign"', time: '5 days ago', type: 'edit' },
];
const TYPE_COLORS: Record<string, string> = {
edit: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
export: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
invite: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
system: 'bg-muted text-muted-foreground',
delete: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
create: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400',
};
export default function ResponsiveDialogScrollableExample() {
return (
<div className='space-y-4'>
<h3 className='text-lg font-semibold'>Scrollable Content</h3>
<div className='flex flex-wrap gap-2'>
<ResponsiveDialog position='bottom-right' size='md'>
<ResponsiveDialogTrigger render={<Button variant='outline' />}>Activity Log</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Activity Log</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Recent actions across your workspace.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
<ResponsiveDialogBody className='space-y-1'>
{ACTIVITY_LOG.map((entry, i) => (
<div key={entry.id}>
<div className='flex items-start gap-3 py-2.5'>
<span
className={`mt-0.5 shrink-0 rounded px-1.5 py-0.5 text-xs font-medium capitalize ${TYPE_COLORS[entry.type]}`}
>
{entry.type}
</span>
<div className='min-w-0 flex-1'>
<p className='text-sm leading-snug'>{entry.action}</p>
<p className='text-muted-foreground mt-0.5 text-xs'>
{entry.user} · {entry.time}
</p>
</div>
</div>
{i < ACTIVITY_LOG.length - 1 && <Separator />}
</div>
))}
</ResponsiveDialogBody>
<ResponsiveDialogFooter>
<ResponsiveDialogClose render={<Button variant='outline' className='max-md:w-full' />}>
Close
</ResponsiveDialogClose>
</ResponsiveDialogFooter>
</ResponsiveDialogContent>
</ResponsiveDialog>
<LongFormDialog />
</div>
</div>
);
}
const longFormSchema = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.string().email(),
phone: z.string(),
company: z.string(),
role: z.string().min(1),
country: z.string().min(1),
bio: z.string(),
});
function LongFormDialog() {
const [open, setOpen] = React.useState(false);
const form = useForm({
defaultValues: { firstName: '', lastName: '', email: '', phone: '', company: '', role: '', country: '', bio: '' },
resolver: zodResolver(longFormSchema),
});
const lens = useLens({ control: form.control });
const onSubmit = async (data: z.infer<typeof longFormSchema>) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
alert(JSON.stringify(data, null, 2));
setOpen(false);
form.reset();
};
return (
<ResponsiveDialog open={open} onOpenChange={setOpen} position='right' size='md'>
<ResponsiveDialogTrigger render={<Button variant='outline' />}>Long Form</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<form onSubmit={form.handleSubmit(onSubmit)} className='flex flex-col flex-1 min-h-0'>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Create Profile</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Fill in your details to create your team profile.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
<ResponsiveDialogBody className='space-y-4'>
<div className='grid grid-cols-2 gap-3'>
<InputField lens={lens.focus('firstName')} label='First name' />
<InputField lens={lens.focus('lastName')} label='Last name' />
</div>
<InputField lens={lens.focus('email')} label='Email' type='email' />
<InputField lens={lens.focus('phone')} label='Phone' type='tel' />
<InputField lens={lens.focus('company')} label='Company' />
<SelectField
lens={lens.focus('role')}
label='Role'
options={{
Admin: 'admin',
Developer: 'developer',
Designer: 'designer',
Manager: 'manager',
Viewer: 'viewer',
}}
/>
<SelectField
lens={lens.focus('country')}
label='Country'
options={{
France: 'fr',
Germany: 'de',
'United States': 'us',
'United Kingdom': 'gb',
Canada: 'ca',
Australia: 'au',
}}
/>
<TextareaField lens={lens.focus('bio')} label='Bio' rows={4} />
</ResponsiveDialogBody>
<ResponsiveDialogFooter>
<ResponsiveDialogClose render={<Button variant='outline' className='max-md:w-full' />}>
Cancel
</ResponsiveDialogClose>
<SubmitButton className='max-md:w-full'>Create Profile</SubmitButton>
</ResponsiveDialogFooter>
</form>
</ResponsiveDialogContent>
</ResponsiveDialog>
);
}
Props
Prop
Type
ResponsiveDialogBody Props
Prop
Type