About
A high-end, immersive hero component featuring a 3D parallax image card, refractive glass overlays, and dynamic typography. Designed for photography portfolios or studio landing pages.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Cinematic Image Showcase to your project and fits it to your theme.
Code
How Cinematic Image Showcase is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
import React, { useState, useEffect, useRef } from 'react';
const OpticStudio: React.FC = () => {
const [activeIndex, setActiveIndex] = useState(0);
const [mousePos, setMousePos] = useState({ x: 50, y: 50 });
const [tilt, setTilt] = useState({ x: 0, y: 0 });
const sceneRef = useRef<HTMLDivElement>(null);
const images = [
"https://cdn.21st.dev/assets/mirror/80/809bd85ebb846e015e1ebed1be96035c0a9fcafecc66a80b2bcb61807c1125f2.jpg",
"https://cdn.21st.dev/assets/mirror/b1/b1b989c58e989fab33a8d0a0df3db5d492e005cd9483067dd36072197ca64920.jpg",
"https://cdn.21st.dev/assets/mirror/e5/e5ef2d30267a4e7f81bc0a61283385f0e59c348d8e69a017f1a87b5323fd8abc.jpg"
];
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
// Update global mouse percentage for grid/spotlight
const xPct = (e.clientX / window.innerWidth) * 100;
const yPct = (e.clientY / window.innerHeight) * 100;
setMousePos({ x: xPct, y: yPct });
// Calculate 3D Tilt
const xAxis = (window.innerWidth / 2 - e.clientX) / 25;
const yAxis = (window.innerHeight / 2 - e.clientY) / 25;
setTilt({ x: xAxis, y: yAxis });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
const resetTilt = () => setTilt({ x: 0, y: 0 });
return (
<div style={containerStyle}>
{/* Dynamic Styles Injection */}
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@900&family=JetBrains+Mono:wght@300;500&family=Playfair+Display:ital,wght@0,900;1,900&display=swap');
:root {
--bg: #050505;
--accent: #e0e0e0;
--primary: #ffffff;
--dim: #444;
--glass: rgba(255, 255, 255, 0.03);
--border: rgba(255, 255, 255, 0.1);
--font-main: 'Inter', sans-serif;
--font-serif: 'Playfair Display', serif;
--font-mono: 'JetBrains Mono', monospace;
}
@keyframes pulse {
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.3; }
100% { transform: translate(-50%, -55%) scale(1.2); opacity: 0.5; }
}
.word-reveal {
display: block;
overflow: hidden;
animation: reveal 1s cubic-bezier(0.23, 1, 0.32, 1) forwards;
opacity: 0;
transform: translateY(50px);
}
@keyframes reveal {
to { opacity: 1; transform: translateY(0); }
}
`}</style>
{/* Background Layers */}
<div style={{
...opticGridStyle,
'--mouse-x': `${mousePos.x}%`,
'--mouse-y': `${mousePos.y}%`
} as React.CSSProperties} className="optic-grid-effect" />
<div style={ambientGlowStyle} />
{/* HUD Navigation */}
<div style={hudStyle}>
<div style={hudTopStyle}>
<div style={{ fontFamily: 'var(--font-mono)', fontWeight: 500, fontSize: '1.2rem', cursor: 'pointer', pointerEvents: 'auto' }}>
OPTIC // STUDIO
</div>
<nav style={navLinksStyle}>
{['ARCHIVE', 'EXHIBITIONS', 'PROCESS', 'STUDIO'].map(link => (
<a key={link} href={`#${link}`} style={navLinkItemStyle}>{link}</a>
))}
</nav>
</div>
<div style={viewfinderStyle} />
<div style={{ ...focusCornerStyle, top: '4rem', left: '4rem', borderRight: 'none', borderBottom: 'none' }} />
<div style={{ ...focusCornerStyle, top: '4rem', right: '4rem', borderLeft: 'none', borderBottom: 'none' }} />
<div style={{ ...focusCornerStyle, bottom: '4rem', left: '4rem', borderRight: 'none', borderTop: 'none' }} />
<div style={{ ...focusCornerStyle, bottom: '4rem', right: '4rem', borderLeft: 'none', borderTop: 'none' }} />
</div>
{/* Main Content */}
<main style={heroStyle}>
<div style={{ zIndex: 20 }}>
<h1 style={headlineStyle}>
<span className="word-reveal" style={{ animationDelay: '0.2s' }}>Moments</span>
<span className="word-reveal" style={{
animationDelay: '0.35s',
fontFamily: 'var(--font-serif)',
fontStyle: 'italic',
color: 'transparent',
WebkitTextStroke: '1px var(--primary)',
marginLeft: '10%'
}}>That</span>
<span className="word-reveal" style={{ animationDelay: '0.5s', marginTop: '-0.1em' }}>Breathe.</span>
</h1>
</div>
<div
ref={sceneRef}
style={heroRightStyle}
onMouseLeave={resetTilt}
>
<div style={{
...imageContainerStyle,
transform: `rotateY(${tilt.x}deg) rotateX(${tilt.y}deg)`
}}>
<img
src={images[activeIndex]}
alt="Cinematic"
style={mainImageStyle}
/>
<div style={{
...refractiveOverlayStyle,
maskImage: `radial-gradient(circle at ${mousePos.x}% ${mousePos.y}%, transparent 150px, black 300px)`,
WebkitMaskImage: `radial-gradient(circle at ${mousePos.x}% ${mousePos.y}%, transparent 150px, black 300px)`
}} />
<div style={{ ...dataLabelStyle, top: '10%', right: '-5%' }}>LOC: 35.6895° N, 139.6917° E</div>
<div style={{ ...dataLabelStyle, bottom: '20%', left: '-10%' }}>CAM: 50MM F/1.2 LENS</div>
<div style={{ ...dataLabelStyle, top: '40%', left: '-15%' }}>STORY: THE SILENT VIGIL</div>
</div>
</div>
</main>
{/* Footer Preview */}
<div style={scrollPreviewStyle}>
{images.map((img, i) => (
<div
key={i}
onClick={() => setActiveIndex(i)}
style={{
...previewBoxStyle,
width: activeIndex === i ? '100px' : '60px',
borderColor: activeIndex === i ? 'var(--primary)' : 'var(--border)'
}}
>
<img src={img} alt="preview" style={{ ...previewImgStyle, opacity: activeIndex === i ? 1 : 0.4 }} />
</div>
))}
</div>
</div>
);
};
// --- Styles Object ---
const containerStyle: React.CSSProperties = {
backgroundColor: '#050505',
color: '#ffffff',
fontFamily: "'Inter', sans-serif",
height: '100vh',
width: '100vw',
overflow: 'hidden',
position: 'relative'
};
const opticGridStyle: React.CSSProperties = {
position: 'fixed',
inset: 0,
backgroundImage: `linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)`,
backgroundSize: '80px 80px',
backgroundPosition: 'center center',
opacity: 0.2,
zIndex: 1,
pointerEvents: 'none'
};
const ambientGlowStyle: React.CSSProperties = {
position: 'fixed',
width: '60vw',
height: '60vw',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: 'radial-gradient(circle, rgba(255,255,255,0.05) 0%, transparent 70%)',
filter: 'blur(100px)',
zIndex: 0,
animation: 'pulse 10s infinite alternate ease-in-out'
};
const heroStyle: React.CSSProperties = {
position: 'relative',
display: 'grid',
gridTemplateColumns: '1fr 1.2fr',
height: '100vh',
width: '100%',
zIndex: 10,
padding: '4rem',
alignItems: 'center'
};
const headlineStyle: React.CSSProperties = {
fontSize: 'clamp(4rem, 10vw, 12rem)',
lineHeight: 0.85,
fontWeight: 900,
textTransform: 'uppercase',
letterSpacing: '-0.04em',
pointerEvents: 'none'
};
const heroRightStyle: React.CSSProperties = {
position: 'relative',
height: '80vh',
perspective: '2000px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
};
const imageContainerStyle: React.CSSProperties = {
position: 'relative',
width: '80%',
height: '100%',
transformStyle: 'preserve-3d',
transition: 'transform 0.6s cubic-bezier(0.23, 1, 0.32, 1)'
};
const mainImageStyle: React.CSSProperties = {
width: '100%',
height: '100%',
objectFit: 'cover',
filter: 'grayscale(0.2) contrast(1.1)',
boxShadow: '0 50px 100px rgba(0,0,0,0.5)',
transition: 'opacity 0.3s ease'
};
const refractiveOverlayStyle: React.CSSProperties = {
position: 'absolute',
inset: '-20px',
backdropFilter: 'blur(0px)',
border: '1px solid rgba(255,255,255,0.1)',
zIndex: 25,
pointerEvents: 'none',
background: 'linear-gradient(135deg, rgba(255,255,255,0.05) 0%, transparent 100%)'
};
const dataLabelStyle: React.CSSProperties = {
position: 'absolute',
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem',
textTransform: 'uppercase',
background: 'rgba(255, 255, 255, 0.03)',
backdropFilter: 'blur(10px)',
padding: '8px 12px',
border: '1px solid rgba(255, 255, 255, 0.1)',
letterSpacing: '0.1em',
display: 'flex',
alignItems: 'center',
gap: '10px',
zIndex: 40,
color: '#fff'
};
const hudStyle: React.CSSProperties = {
position: 'fixed',
inset: 0,
pointerEvents: 'none',
zIndex: 100,
padding: '2rem'
};
const hudTopStyle: React.CSSProperties = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start'
};
const navLinksStyle: React.CSSProperties = {
display: 'flex',
gap: '3rem',
fontFamily: 'var(--font-mono)',
fontSize: '0.8rem',
pointerEvents: 'auto'
};
const navLinkItemStyle: React.CSSProperties = {
color: '#fff',
textDecoration: 'none',
opacity: 0.6
};
const focusCornerStyle: React.CSSProperties = {
position: 'absolute',
width: '40px',
height: '40px',
border: '1px solid #444'
};
const scrollPreviewStyle: React.CSSProperties = {
position: 'absolute',
bottom: '4rem',
left: '4rem',
display: 'flex',
gap: '1rem',
zIndex: 50
};
const previewBoxStyle: React.CSSProperties = {
height: '80px',
background: '#444',
position: 'relative',
overflow: 'hidden',
border: '1px solid rgba(255,255,255,0.1)',
transition: '0.5s cubic-bezier(0.23, 1, 0.32, 1)',
cursor: 'pointer'
};
const previewImgStyle: React.CSSProperties = {
width: '100%',
height: '100%',
objectFit: 'cover',
transition: '0.5s'
};
const viewfinderStyle: React.CSSProperties = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '30px',
height: '30px',
border: '1px solid transparent'
};
export default OpticStudio;













