Media Modals

A media modal component that opens images or videos with a smooth animation

Installation

npm install framer-motion
modal.tsx
1
// @ts-nocheck
2
'use client';
3
import React, { useEffect, useId, useState } from 'react';
4
import { AnimatePresence, motion, MotionConfig } from 'framer-motion';
5
import { useMediaQuery } from '@/hooks/use-media-query';
6
import { XIcon } from 'lucide-react';
7
8
interface IMediaModal {
9
imgSrc?: string;
10
videoSrc?: string;
11
className?: string;
12
}
13
const transition = {
14
type: 'spring',
15
duration: 0.4,
16
};
17
export function MediaModal({ imgSrc, videoSrc, className }: IMediaModal) {
18
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false);
19
const isDesktop = useMediaQuery('(min-width:768px)');
20
const uniqueId = useId();
21
22
useEffect(() => {
23
if (isMediaModalOpen) {
24
document.body.classList.add('overflow-hidden');
25
} else {
26
document.body.classList.remove('overflow-hidden');
27
}
28
29
const handleKeyDown = (event: KeyboardEvent) => {
30
if (event.key === 'Escape') {
31
setIsMediaModalOpen(false);
32
}
33
};
34
35
document.addEventListener('keydown', handleKeyDown);
36
return () => {
37
document.removeEventListener('keydown', handleKeyDown);
38
};
39
}, [isMediaModalOpen]);
40
return (
41
<>
42
<MotionConfig transition={transition}>
43
<>
44
<motion.div
45
// @ts-ignore
46
className='w-full h-full flex relative flex-col overflow-hidden border dark:bg-black bg-gray-300 hover:bg-gray-200 dark:hover:bg-gray-950'
47
layoutId={`dialog-${uniqueId}`}
48
style={{
49
borderRadius: '12px',
50
}}
51
onClick={() => {
52
setIsMediaModalOpen(true);
53
}}
54
>
55
{imgSrc && (
56
<motion.div
57
layoutId={`dialog-img-${uniqueId}`}
58
className='w-full h-full'
59
>
60
{/* eslint-disable-next-line @next/next/no-img-element */}
61
<img
62
src={imgSrc}
63
alt='A desk lamp designed by Edouard Wilfrid Buquet in 1925. It features a double-arm design and is made from nickel-plated brass, aluminium and varnished wood.'
64
className=' w-full object-cover h-full'
65
/>
66
</motion.div>
67
)}
68
{videoSrc && (
69
<motion.div
70
layoutId={`dialog-video-${uniqueId}`}
71
className='w-full h-full'
72
>
73
<video
74
autoPlay
75
muted
76
loop
77
className='h-full w-full object-cover rounded-sm'
78
>
79
<source src={videoSrc!} type='video/mp4' />
80
</video>
81
</motion.div>
82
)}
83
</motion.div>
84
</>
85
<AnimatePresence initial={false} mode='sync'>
86
{isMediaModalOpen && (
87
<>
88
<motion.div
89
key={`backdrop-${uniqueId}`}
90
className='fixed inset-0 h-full w-full dark:bg-black/25 bg-white/95 backdrop-blur-sm '
91
variants={{ open: { opacity: 1 }, closed: { opacity: 0 } }}
92
initial='closed'
93
animate='open'
94
exit='closed'
95
onClick={() => {
96
setIsMediaModalOpen(false);
97
}}
98
/>
99
<motion.div
100
key='dialog'
101
className='pointer-events-none fixed inset-0 flex items-center justify-center z-50'
102
>
103
<motion.div
104
className='pointer-events-auto relative flex flex-col overflow-hidden dark:bg-gray-950 bg-gray-200 border w-[80%] h-[90%] '
105
layoutId={`dialog-${uniqueId}`}
106
tabIndex={-1}
107
style={{
108
borderRadius: '24px',
109
}}
110
>
111
{imgSrc && (
112
<motion.div
113
layoutId={`dialog-img-${uniqueId}`}
114
className='w-full h-full'
115
>
116
{/* eslint-disable-next-line @next/next/no-img-element */}
117
<img
118
src={imgSrc}
119
alt=''
120
className='h-full w-full object-cover'
121
/>
122
</motion.div>
123
)}
124
{videoSrc && (
125
<motion.div
126
layoutId={`dialog-video-${uniqueId}`}
127
className='w-full h-full'
128
>
129
<video
130
autoPlay
131
muted
132
loop
133
controls
134
className='h-full w-full object-cover rounded-sm'
135
>
136
<source src={videoSrc!} type='video/mp4' />
137
</video>
138
</motion.div>
139
)}
140
141
<button
142
onClick={() => setIsMediaModalOpen(false)}
143
className='absolute right-6 top-6 p-3 text-zinc-50 cursor-pointer dark:bg-gray-900 bg-gray-400 hover:bg-gray-500 rounded-full dark:hover:bg-gray-800'
144
type='button'
145
aria-label='Close dialog'
146
>
147
<XIcon size={24} />
148
</button>
149
</motion.div>
150
</motion.div>
151
</>
152
)}
153
</AnimatePresence>
154
</MotionConfig>
155
</>
156
);
157
}

Props

PropTypeDefaultDescription
imgSrcstringundefinedOptional source URL for an image to display in the modal.
videoSrcstringundefinedOptional source URL for a video to display in the modal.
classNamestringundefinedOptional CSS class for styling the modal component.

Without Components

Sometimes, we don't need reusable components because they are most useful when a component is used 2-3 times or more. However, in a single-page application, reusable components aren't always necessary. In such cases, you can use this component instead, and it will give you the same effect.