October 19th 2024

Responsive Modal

A responsive Modal/Drawer that renders a Dialog on desktop and a Drawer on mobile.

This component combines ShadCN's Dialog and Drawer component to create a responsive modal that renders a Dialog on desktop and a Drawer on mobile.


Credits


Install dependencies:

npx shadcn@latest init
npx shadcn@latest add drawer dialog

You will need to create a hook to detect the screen size. Create a file called media-query.tsx in the lib directory and add the following code:

media-query.tsx

"use client";
import * as React from "react";
 
export function useMediaQuery(query: string) {
  const [value, setValue] = React.useState(false);
 
  React.useEffect(() => {
    function onChange(event: MediaQueryListEvent) {
      setValue(event.matches);
    }
 
    const result = matchMedia(query);
    result.addEventListener("change", onChange);
    setValue(result.matches);
 
    return () => result.removeEventListener("change", onChange);
  }, [query]);
 
  return value;
}

Create a file called modal.tsx in the components/ui directory and add the following code:

modal.tsx

// components/ui/modal.tsx
"use client";
import {
  Drawer,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
  DrawerClose,
} from "./ui/drawer";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
  DialogClose,
} from "@/components/ui/dialog";
import { useMediaQuery } from "@/lib/media-query";
 
export const ModalRoot = ({ children }: { children: React.ReactNode }) => {
  const isDesktop = useMediaQuery("(min-width: 768px)");
  if (isDesktop) {
    return <Dialog>{children}</Dialog>;
  } else {
    return <Drawer>{children}</Drawer>;
  }
};
 
export const ModalTrigger = ({ children }: { children: React.ReactNode }) => {
  const isdesktop = useMediaQuery("(min-width: 768px)");
  if (isdesktop) {
    return <DialogTrigger className="w-fit">{children}</DialogTrigger>;
  } else {
    return <DrawerTrigger className="w-fit">{children}</DrawerTrigger>;
  }
};
 
export const ModalContent = ({ children }: { children: React.ReactNode }) => {
  const isdesktop = useMediaQuery("(min-width: 768px)");
  if (isdesktop) {
    return <DialogContent className="text-left">{children}</DialogContent>;
  } else {
    return <DrawerContent>{children}</DrawerContent>;
  }
};
 
export const ModalHeader = ({ children }: { children: React.ReactNode }) => {
  const isdesktop = useMediaQuery("(min-width: 768px)");
  if (isdesktop) {
    return <DialogHeader className="text-left">{children}</DialogHeader>;
  } else {
    return <DrawerHeader className="text-left">{children}</DrawerHeader>;
  }
};
 
export const ModalFooter = ({ children }: { children: React.ReactNode }) => {
  const isdesktop = useMediaQuery("(min-width: 768px)");
  if (isdesktop) {
    return <DialogFooter className="text-left">{children}</DialogFooter>;
  } else {
    return <DrawerFooter>{children}</DrawerFooter>;
  }
};
 
export const ModalTitle = ({ children }: { children: React.ReactNode }) => {
  const isdesktop = useMediaQuery("(min-width: 768px)");
  if (isdesktop) {
    return <DialogTitle>{children}</DialogTitle>;
  } else {
    return <DrawerTitle>{children}</DrawerTitle>;
  }
};
 
export const ModalDescription = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  const isdesktop = useMediaQuery("(min-width: 768px)");
  if (isdesktop) {
    return <DialogDescription>{children}</DialogDescription>;
  } else {
    return <DrawerDescription>{children}</DrawerDescription>;
  }
};
 
export const ModalClose = ({ children }: { children: React.ReactNode }) => {
  const isdesktop = useMediaQuery("(min-width: 768px)");
  if (isdesktop) {
    return <DialogClose className="text-right">{children}</DialogClose>;
  } else {
    return <DrawerClose className="pb-4">{children}</DrawerClose>;
  }
};

Once you have created this component you can use it in your application like so:

example.tsx

import {
  ModalRoot,
  ModalContent,
  ModalTrigger,
  ModalDescription,
  ModalFooter,
  ModalHeader,
  ModalTitle,
  ModalClose,
} from "./drawer-dialog";
import { Button } from "./ui/button";
 
export default function ResponsiveModal() {
  return (
    <ModalRoot>
      <ModalTrigger>
        <Button variant={"outline"} className="w-fit">
          Open
        </Button>
      </ModalTrigger>
      <ModalContent>
        <ModalHeader>
          <ModalTitle>Modal Title</ModalTitle>
          <ModalDescription>Modal Description</ModalDescription>
        </ModalHeader>
        <ModalFooter>
          <ModalClose>
            <Button className="w-full" variant={"outline"}>
              Close
            </Button>
          </ModalClose>
        </ModalFooter>
      </ModalContent>
    </ModalRoot>
  );
}