A dialog that pops up in the center of the screen on desktop and slide up on mobile.
1"use client";
2
3import * as DialogPrimitive from "@radix-ui/react-dialog";
4import { cva, type VariantProps } from "class-variance-authority";
5import { X } from "lucide-react";
6import * as React from "react";
7
8import { cn } from "@/lib/utils";
9
10const ResponsiveModal = DialogPrimitive.Root;
11
12const ResponsiveModalTrigger = DialogPrimitive.Trigger;
13
14const ResponsiveModalClose = DialogPrimitive.Close;
15
16const ResponsiveModalPortal = DialogPrimitive.Portal;
17
18const ResponsiveModalOverlay = React.forwardRef<
19 React.ElementRef<typeof DialogPrimitive.Overlay>,
20 React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
21>(({ className, ...props }, ref) => (
22 <DialogPrimitive.Overlay
23 className={cn(
24 "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25 className,
26 )}
27 {...props}
28 ref={ref}
29 />
30));
31ResponsiveModalOverlay.displayName = DialogPrimitive.Overlay.displayName;
32
33const ResponsiveModalVariants = cva(
34 cn(
35 "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 overflow-y-auto",
36 "lg:left-[50%] lg:top-[50%] lg:grid lg:w-full lg:max-w-lg lg:translate-x-[-50%] lg:translate-y-[-50%] lg:border lg:duration-200 lg:data-[state=open]:animate-in lg:data-[state=closed]:animate-out lg:data-[state=closed]:fade-out-0 lg:data-[state=open]:fade-in-0 lg:data-[state=closed]:zoom-out-95 lg:data-[state=open]:zoom-in-95 lg:data-[state=closed]:slide-out-to-left-1/2 lg:data-[state=closed]:slide-out-to-top-[48%] lg:data-[state=open]:slide-in-from-left-1/2 lg:data-[state=open]:slide-in-from-top-[48%] lg:rounded-xl",
37 ),
38 {
39 variants: {
40 side: {
41 top: "inset-x-0 top-0 border-b rounded-b-xl max-h-[90%] lg:h-fit data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
42 bottom:
43 "inset-x-0 bottom-0 border-t lg:h-fit max-h-[90%] rounded-t-xl data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
44 left: "inset-y-0 left-0 h-full lg:h-fit w-3/4 border-r rounded-r-xl data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
45 right:
46 "inset-y-0 right-0 h-full lg:h-fit w-3/4 border-l rounded-l-xl data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
47 },
48 },
49 defaultVariants: {
50 side: "bottom",
51 },
52 },
53);
54
55interface ResponsiveModalContentProps
56 extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
57 VariantProps<typeof ResponsiveModalVariants> {}
58
59const ResponsiveModalContent = React.forwardRef<
60 React.ElementRef<typeof DialogPrimitive.Content>,
61 ResponsiveModalContentProps
62>(({ side = "bottom", className, children, ...props }, ref) => (
63 <ResponsiveModalPortal>
64 <ResponsiveModalOverlay />
65 <DialogPrimitive.Content
66 ref={ref}
67 className={cn(ResponsiveModalVariants({ side }), className)}
68 {...props}
69 >
70 {children}
71 <ResponsiveModalClose className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
72 <X className="h-4 w-4" />
73 <span className="sr-only">Close</span>
74 </ResponsiveModalClose>
75 </DialogPrimitive.Content>
76 </ResponsiveModalPortal>
77));
78ResponsiveModalContent.displayName = DialogPrimitive.Content.displayName;
79
80const ResponsiveModalHeader = ({
81 className,
82 ...props
83}: React.HTMLAttributes<HTMLDivElement>) => (
84 <div
85 className={cn(
86 "flex flex-col space-y-2 text-center sm:text-left",
87 className,
88 )}
89 {...props}
90 />
91);
92ResponsiveModalHeader.displayName = "ResponsiveModalHeader";
93
94const ResponsiveModalFooter = ({
95 className,
96 ...props
97}: React.HTMLAttributes<HTMLDivElement>) => (
98 <div
99 className={cn(
100 "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
101 className,
102 )}
103 {...props}
104 />
105);
106ResponsiveModalFooter.displayName = "ResponsiveModalFooter";
107
108const ResponsiveModalTitle = React.forwardRef<
109 React.ElementRef<typeof DialogPrimitive.Title>,
110 React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
111>(({ className, ...props }, ref) => (
112 <DialogPrimitive.Title
113 ref={ref}
114 className={cn("text-lg font-semibold text-foreground", className)}
115 {...props}
116 />
117));
118ResponsiveModalTitle.displayName = DialogPrimitive.Title.displayName;
119
120const ResponsiveModalDescription = React.forwardRef<
121 React.ElementRef<typeof DialogPrimitive.Description>,
122 React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
123>(({ className, ...props }, ref) => (
124 <DialogPrimitive.Description
125 ref={ref}
126 className={cn("text-sm text-muted-foreground", className)}
127 {...props}
128 />
129));
130ResponsiveModalDescription.displayName =
131 DialogPrimitive.Description.displayName;
132
133export {
134 ResponsiveModal,
135 ResponsiveModalClose,
136 ResponsiveModalContent,
137 ResponsiveModalDescription,
138 ResponsiveModalFooter,
139 ResponsiveModalHeader,
140 ResponsiveModalOverlay,
141 ResponsiveModalPortal,
142 ResponsiveModalTitle,
143 ResponsiveModalTrigger,
144};
145