Docs
Go back
Component by
OV
OblivionOBLIVION
A story begins in silence.
TSX
cinematic-intro
"use client"
import React, { useEffect, useRef, useState } from "react"
import { motion, useAnimation } from "framer-motion"
export const componentMeta = {
name: "Cinematic Intro",
description: "A breathtaking full-screen cinematic intro sequence with letterboxing and grain. Perfect for immersive landing pages.",
props: {
title: { type: "string", description: "Main title text", default: '"OBLIVION"' },
subtitle: { type: "string", description: "Subtitle text", default: '"A story begins in silence."' },
duration: { type: "number", description: "Total duration in ms", default: "4000" },
onComplete: { type: "function", description: "Callback when animation finishes" }
}
}
const BARS_OPEN_DELAY = 0.2
const TITLE_DELAY = 0.7
const SUBTITLE_DELAY = 1.1
const LINE_DELAY = 0.95
const HOLD_DURATION = 1.4
const EXIT_DURATION = 0.9
function LetterboxBars() {
return (
<>
<motion.div
style={styles.barTop}
initial={{ scaleY: 1 }}
animate={{ scaleY: 0 }}
transition={{ duration: 0.9, delay: BARS_OPEN_DELAY, ease: [0.76, 0, 0.24, 1] }}
exit={{ scaleY: 1 }}
/>
<motion.div
style={styles.barBottom}
initial={{ scaleY: 1 }}
animate={{ scaleY: 0 }}
transition={{ duration: 0.9, delay: BARS_OPEN_DELAY, ease: [0.76, 0, 0.24, 1] }}
exit={{ scaleY: 1 }}
/>
</>
)
}
function GrainOverlay() {
return (
<svg style={styles.grain} xmlns="http://www.w3.org/2000/svg">
<filter id="grain-filter">
<feTurbulence
type="fractalNoise"
baseFrequency="0.65"
numOctaves="3"
stitchTiles="stitch"
/>
<feColorMatrix type="saturate" values="0" />
<feBlend in="SourceGraphic" mode="overlay" />
</filter>
<rect width="100%" height="100%" filter="url(#grain-filter)" opacity="0.06" />
</svg>
)
}
function TitleCard({ title, subtitle }: { title: string; subtitle?: string }) {
return (
<motion.div
style={styles.titleWrapper}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: TITLE_DELAY - 0.1 }}
>
<motion.div
style={styles.line}
initial={{ scaleX: 0, opacity: 0 }}
animate={{ scaleX: 1, opacity: 1 }}
transition={{ duration: 0.7, delay: LINE_DELAY, ease: [0.16, 1, 0.3, 1] }}
/>
<div style={styles.titleRow} aria-label={title}>
{title.split("").map((char, i) => (
<motion.span
key={i}
style={char === " " ? styles.titleSpace : styles.titleChar}
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.55,
delay: TITLE_DELAY + i * 0.04,
ease: [0.16, 1, 0.3, 1],
}}
>
{char === " " ? " " : char}
</motion.span>
))}
</div>
{subtitle && (
<motion.p
style={styles.subtitle}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: SUBTITLE_DELAY, ease: "easeOut" }}
>
{subtitle}
</motion.p>
)}
<motion.div
style={{ ...styles.line, marginTop: "1.6rem" }}
initial={{ scaleX: 0, opacity: 0 }}
animate={{ scaleX: 1, opacity: 1 }}
transition={{ duration: 0.7, delay: LINE_DELAY + 0.12, ease: [0.16, 1, 0.3, 1] }}
/>
</motion.div>
)
}
function CinematicIntroContent({
title = "OBLIVION",
subtitle = "A story begins in silence.",
duration = 4000,
onComplete,
}: {
title?: string
subtitle?: string
duration?: number
onComplete?: () => void
}) {
const controls = useAnimation()
const timerRef = useRef<NodeJS.Timeout | null>(null)
const totalBeforeExit = (SUBTITLE_DELAY + HOLD_DURATION) * 1000
useEffect(() => {
// We intentionally start the fadeout near the end of the duration
timerRef.current = setTimeout(async () => {
await controls.start("exit")
onComplete?.()
}, Math.max(totalBeforeExit, duration - EXIT_DURATION * 1000))
return () => {
if (timerRef.current) clearTimeout(timerRef.current)
}
}, [controls, duration, onComplete, totalBeforeExit])
return (
<motion.div
style={styles.root}
variants={{
exit: { opacity: 0 },
}}
animate={controls}
transition={{ duration: EXIT_DURATION, ease: "easeInOut" }}
>
<GrainOverlay />
<LetterboxBars />
<TitleCard title={title} subtitle={subtitle} />
<div style={styles.vignette} />
</motion.div>
)
}
export default function CinematicIntro(props: {
title?: string
subtitle?: string
duration?: number
onComplete?: () => void
}) {
const [key, setKey] = useState(0)
return (
<CinematicIntroContent
key={key}
{...props}
onComplete={() => {
if (props.onComplete) props.onComplete()
setTimeout(() => {
setKey((prev) => prev + 1)
}, 800)
}}
/>
)
}
const styles: Record<string, React.CSSProperties> = {
root: {
position: "absolute", // Adjusted from "fixed" to play nicely in component previews
inset: 0,
zIndex: 9999,
backgroundColor: "#080808",
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
fontFamily: "'Times New Roman', Times, serif",
borderRadius: "inherit",
},
barTop: {
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "13%",
backgroundColor: "#000",
transformOrigin: "top",
zIndex: 10,
},
barBottom: {
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: "13%",
backgroundColor: "#000",
transformOrigin: "bottom",
zIndex: 10,
},
grain: {
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
zIndex: 1,
},
vignette: {
position: "absolute",
inset: 0,
background: "radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.75) 100%)",
pointerEvents: "none",
zIndex: 2,
},
titleWrapper: {
position: "relative",
zIndex: 5,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "0.6rem",
textAlign: "center",
padding: "0 2rem",
},
line: {
width: "9rem",
height: "1px",
backgroundColor: "rgba(255,255,255,0.25)",
transformOrigin: "left",
marginBottom: "0.8rem",
},
titleRow: {
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
overflow: "hidden",
},
titleChar: {
display: "inline-block",
fontSize: "clamp(2.8rem, 7vw, 6rem)",
fontWeight: 400,
letterSpacing: "0.28em",
color: "#f0ece3",
lineHeight: 1,
textTransform: "uppercase",
},
titleSpace: {
display: "inline-block",
width: "0.4em",
},
subtitle: {
fontSize: "clamp(0.65rem, 1.2vw, 0.8rem)",
letterSpacing: "0.35em",
color: "rgba(240,236,227,0.38)",
textTransform: "uppercase",
margin: 0,
fontFamily: "'DM Sans', sans-serif",
},
}"use client"
import React, { useEffect, useRef, useState } from "react"
import { motion, useAnimation } from "framer-motion"
export const componentMeta = {
name: "Cinematic Intro",
description: "A breathtaking full-screen cinematic intro sequence with letterboxing and grain. Perfect for immersive landing pages.",
props: {
title: { type: "string", description: "Main title text", default: '"OBLIVION"' },
subtitle: { type: "string", description: "Subtitle text", default: '"A story begins in silence."' },
duration: { type: "number", description: "Total duration in ms", default: "4000" },
onComplete: { type: "function", description: "Callback when animation finishes" }
}
}
const BARS_OPEN_DELAY = 0.2
const TITLE_DELAY = 0.7
const SUBTITLE_DELAY = 1.1
const LINE_DELAY = 0.95
const HOLD_DURATION = 1.4
const EXIT_DURATION = 0.9
function LetterboxBars() {
return (
<>
<motion.div
style={styles.barTop}
initial={{ scaleY: 1 }}
animate={{ scaleY: 0 }}
transition={{ duration: 0.9, delay: BARS_OPEN_DELAY, ease: [0.76, 0, 0.24, 1] }}
exit={{ scaleY: 1 }}
/>
<motion.div
style={styles.barBottom}
initial={{ scaleY: 1 }}
animate={{ scaleY: 0 }}
transition={{ duration: 0.9, delay: BARS_OPEN_DELAY, ease: [0.76, 0, 0.24, 1] }}
exit={{ scaleY: 1 }}
/>
</>
)
}
function GrainOverlay() {
return (
<svg style={styles.grain} xmlns="http://www.w3.org/2000/svg">
<filter id="grain-filter">
<feTurbulence
type="fractalNoise"
baseFrequency="0.65"
numOctaves="3"
stitchTiles="stitch"
/>
<feColorMatrix type="saturate" values="0" />
<feBlend in="SourceGraphic" mode="overlay" />
</filter>
<rect width="100%" height="100%" filter="url(#grain-filter)" opacity="0.06" />
</svg>
)
}
function TitleCard({ title, subtitle }: { title: string; subtitle?: string }) {
return (
<motion.div
style={styles.titleWrapper}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: TITLE_DELAY - 0.1 }}
>
<motion.div
style={styles.line}
initial={{ scaleX: 0, opacity: 0 }}
animate={{ scaleX: 1, opacity: 1 }}
transition={{ duration: 0.7, delay: LINE_DELAY, ease: [0.16, 1, 0.3, 1] }}
/>
<div style={styles.titleRow} aria-label={title}>
{title.split("").map((char, i) => (
<motion.span
key={i}
style={char === " " ? styles.titleSpace : styles.titleChar}
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.55,
delay: TITLE_DELAY + i * 0.04,
ease: [0.16, 1, 0.3, 1],
}}
>
{char === " " ? " " : char}
</motion.span>
))}
</div>
{subtitle && (
<motion.p
style={styles.subtitle}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: SUBTITLE_DELAY, ease: "easeOut" }}
>
{subtitle}
</motion.p>
)}
<motion.div
style={{ ...styles.line, marginTop: "1.6rem" }}
initial={{ scaleX: 0, opacity: 0 }}
animate={{ scaleX: 1, opacity: 1 }}
transition={{ duration: 0.7, delay: LINE_DELAY + 0.12, ease: [0.16, 1, 0.3, 1] }}
/>
</motion.div>
)
}
function CinematicIntroContent({
title = "OBLIVION",
subtitle = "A story begins in silence.",
duration = 4000,
onComplete,
}: {
title?: string
subtitle?: string
duration?: number
onComplete?: () => void
}) {
const controls = useAnimation()
const timerRef = useRef<NodeJS.Timeout | null>(null)
const totalBeforeExit = (SUBTITLE_DELAY + HOLD_DURATION) * 1000
useEffect(() => {
// We intentionally start the fadeout near the end of the duration
timerRef.current = setTimeout(async () => {
await controls.start("exit")
onComplete?.()
}, Math.max(totalBeforeExit, duration - EXIT_DURATION * 1000))
return () => {
if (timerRef.current) clearTimeout(timerRef.current)
}
}, [controls, duration, onComplete, totalBeforeExit])
return (
<motion.div
style={styles.root}
variants={{
exit: { opacity: 0 },
}}
animate={controls}
transition={{ duration: EXIT_DURATION, ease: "easeInOut" }}
>
<GrainOverlay />
<LetterboxBars />
<TitleCard title={title} subtitle={subtitle} />
<div style={styles.vignette} />
</motion.div>
)
}
export default function CinematicIntro(props: {
title?: string
subtitle?: string
duration?: number
onComplete?: () => void
}) {
const [key, setKey] = useState(0)
return (
<CinematicIntroContent
key={key}
{...props}
onComplete={() => {
if (props.onComplete) props.onComplete()
setTimeout(() => {
setKey((prev) => prev + 1)
}, 800)
}}
/>
)
}
const styles: Record<string, React.CSSProperties> = {
root: {
position: "absolute", // Adjusted from "fixed" to play nicely in component previews
inset: 0,
zIndex: 9999,
backgroundColor: "#080808",
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
fontFamily: "'Times New Roman', Times, serif",
borderRadius: "inherit",
},
barTop: {
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "13%",
backgroundColor: "#000",
transformOrigin: "top",
zIndex: 10,
},
barBottom: {
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: "13%",
backgroundColor: "#000",
transformOrigin: "bottom",
zIndex: 10,
},
grain: {
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
zIndex: 1,
},
vignette: {
position: "absolute",
inset: 0,
background: "radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.75) 100%)",
pointerEvents: "none",
zIndex: 2,
},
titleWrapper: {
position: "relative",
zIndex: 5,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "0.6rem",
textAlign: "center",
padding: "0 2rem",
},
line: {
width: "9rem",
height: "1px",
backgroundColor: "rgba(255,255,255,0.25)",
transformOrigin: "left",
marginBottom: "0.8rem",
},
titleRow: {
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
overflow: "hidden",
},
titleChar: {
display: "inline-block",
fontSize: "clamp(2.8rem, 7vw, 6rem)",
fontWeight: 400,
letterSpacing: "0.28em",
color: "#f0ece3",
lineHeight: 1,
textTransform: "uppercase",
},
titleSpace: {
display: "inline-block",
width: "0.4em",
},
subtitle: {
fontSize: "clamp(0.65rem, 1.2vw, 0.8rem)",
letterSpacing: "0.35em",
color: "rgba(240,236,227,0.38)",
textTransform: "uppercase",
margin: 0,
fontFamily: "'DM Sans', sans-serif",
},
}Save to favoritesCopy to Figma
React
Cinematic Intro
A breathtaking full-screen cinematic intro sequence with letterboxing and grain. Perfect for immersive landing pages.
Installation
$ npx @cosmoo/oblivion add cinematic-introDependencies
npm install framer-motionUsage Example
tsx
import CinematicIntro from "@/components/oblivion/cinematic-intro"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<CinematicIntro />
</div>
)
}import CinematicIntro from "@/components/oblivion/cinematic-intro"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<CinematicIntro />
</div>
)
}