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.

Loading...

About

ResponsiveDialog is a smart dialog component that automatically adapts to the user's screen size:

  • Desktop (≥1024px): Renders as a SideDialog with flexible positioning
  • Mobile (<1024px): Renders as a Drawer for 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: ResponsiveDialogBody keeps 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 wrapping Header/Body/Footer inside ResponsiveDialogContent must carry className="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

Loading...
'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

Loading...
'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

On this page