shadimage
Shuip
blockresponsive-dialogsource: ok · hit

Responsive Dialog

npx shadcn add "https://shuip.plvo.dev/r/responsive-dialog.json"
free

Interaction

modal

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
react

Registry dependencies

drawerside-dialog

Files1 · source cached 10/9/2026, 7:47:20 AM

./items/blocks/responsive-dialog/component.tsx
'use client';

import * as React from 'react';
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from '@/components/ui/drawer';
import {
  SideDialog,
  SideDialogBody,
  SideDialogClose,
  SideDialogContent,
  SideDialogDescription,
  SideDialogFooter,
  SideDialogHeader,
  SideDialogTitle,
  SideDialogTrigger,
} 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 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;
}

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} repositionInputs={false}>
          {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 {...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({ 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>
  );
}

More from Shuip

Browse all components in this library →