Media Modals Component

A Framer Motion media modal component that opens images and videos with smooth animations. Features elegant transitions, responsive design, and customizable modal effects for modern media viewing experiences.

Installation

npm install motion
modal.tsx
1
// @ts-nocheck
2
'use client';
3
import React, { useEffect, useId, useState } from 'react';
4
import { AnimatePresence, motion, MotionConfig } from 'motion/react';
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.