How to build a video lightbox about section in React
This React about section shows a thumbnail with an animated play button; clicking it opens an iframe-based video modal with a scale+fade entrance powered by Framer Motion AnimatePresence. Stat items below the thumbnail enter one by one with a staggered whileInView animation.
- Stack: React 18, Framer Motion 11, Lucide React (Play + X icons), Tailwind v4, ~175 lines.
- State: a single boolean `isPlaying` toggles the modal; no external state library needed.
- Accessible: the close button carries an aria-label; the iframe has a title attribute; clicking the backdrop also closes the modal.
- Responsive: the thumbnail is aspect-video (16:9) with overflow-hidden; the modal caps at max-w-4xl and keeps its ratio on any screen.
- Theming: all colors come from CSS custom properties (--color-background-dark, --color-accent, etc.), no hardcoded values.
About Video is a centered section that leads with a headline and badge, then surfaces a full-width video thumbnail with a prominent play button. Clicking the thumbnail opens a lightbox modal over the page. A row of stat counters beneath the video, each animated in on scroll, rounds out the social proof without requiring a separate section.
Anatomy
The section is divided into three vertical zones. At the top, a constrained max-w-2xl text block holds the optional badge (pill shape with border and accent color), the h2 title, and a subtitle paragraph. Below that, the thumbnail block fills max-w-5xl at 16:9 aspect-video with rounded corners and a semi-transparent overlay; the overlay always shows the play button, and the group-hover darkens it slightly while the button scales up. At the bottom, a 3-column grid renders the stat items with their large accent-colored values and muted labels.
How it works
Every visual block uses Framer Motion's `whileInView` with `viewport={{ once: true }}` so elements animate only the first time they scroll into view. The text header fades up (y: 20 to 0, opacity 0 to 1) in 500ms. The thumbnail enters with a slightly longer 600ms fade-up and a 100ms delay. Each stat item gets an additional staggered delay: `0.2 + i * 0.08`s, so they cascade naturally. The modal uses `AnimatePresence` so the exit animation (opacity 0, scale 0.9) actually runs before React unmounts the element, without `AnimatePresence`, the exit would be instant.
How to build it in React
Build the thumbnail with a play overlay
Render a relative-positioned div at aspect-video. Set the background image via an inline style on an absolute inset-0 div so the image never breaks the 16:9 ratio. Stack a second absolute inset-0 div with bg-black/30 for the overlay; add a group class on the wrapper and transition the overlay to bg-black/40 on hover.
<motion.div className="relative mt-12 aspect-video rounded-[var(--radius-xl)] overflow-hidden cursor-pointer group" onClick={() => setIsPlaying(true)} > <div className="absolute inset-0 bg-cover bg-center" style={{ backgroundImage: `url(${thumbnailUrl})` }} /> <div className="absolute inset-0 flex items-center justify-center bg-black/30 group-hover:bg-black/40 transition-colors"> <div className="flex h-20 w-20 items-center justify-center rounded-full group-hover:scale-110 transition-transform duration-300" style={{ backgroundColor: "var(--color-accent)" }}> <Play className="h-8 w-8 ml-1" /> </div> </div> </motion.div>Open and close the modal with AnimatePresence
Wrap the modal JSX in `<AnimatePresence>` and gate it with `{isPlaying && videoUrl && ...}`. Give the backdrop a motion.div with opacity 0 to 1 enter / 0 exit. The inner panel uses scale 0.9 to 1 enter / 0.9 exit. Clicking the backdrop sets isPlaying to false; stopPropagation on the inner panel prevents accidental dismissal.
<AnimatePresence> {isPlaying && videoUrl && ( <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4" onClick={() => setIsPlaying(false)} > <motion.div initial={{ scale: 0.9 }} animate={{ scale: 1 }} exit={{ scale: 0.9 }} className="relative w-full max-w-4xl aspect-video" onClick={(e) => e.stopPropagation()} > <iframe src={videoUrl} allow="autoplay; fullscreen" allowFullScreen title="Video de presentation" className="w-full h-full rounded-[var(--radius-lg)]" /> </motion.div> </motion.div> )} </AnimatePresence>Stagger the stat counters with whileInView
Map over the stats array and give each motion.div an increasing delay: `0.2 + index * 0.08`. Pass `viewport={{ once: true }}` so the animation does not replay on scroll-up. The accent-colored value sits in a large bold p tag, the label in a muted sm p below it.
{stats.map((stat, i) => ( <motion.div key={stat.label} initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: 0.2 + i * 0.08, duration: 0.4 }} className="text-center" > <p className="text-3xl font-bold" style={{ color: "var(--color-accent)" }}> {stat.value} </p> <p className="mt-1 text-sm" style={{ color: "var(--color-foreground-light)" }}> {stat.label} </p> </motion.div> ))}
When to use it
Use it on company or product about pages where you have a short explainer or demo video to surface. It works well as a mid-page trust section on landing pages, after the hero, before pricing. Skip it when you have no video asset worth showing, or when the page already has a heavy media section nearby. On very conversion-focused pages the modal interaction can distract; in that case a simple inline video autoplay might serve better.
Used by
- Loom, Uses a hero video thumbnail with a large play button that opens a product demo in a fullscreen lightbox modal.
- Notion, Features a centered about/product video section with a thumbnail, play overlay, and key stats beneath the video player.
- Webflow, Places a product walkthrough video with a play button overlay on its marketing pages, opening into a modal.
FAQ
Why use an iframe instead of a native HTML video element?
An iframe lets you embed hosted videos (YouTube, Vimeo, Wistia) without self-hosting the file. Pass the embed URL with autoplay parameters in the query string. For self-hosted files, swap the iframe for a video element with `autoPlay` and `controls`.
How do I stop the video when the modal closes?
Because the iframe unmounts when `isPlaying` goes false, the browser stops playback automatically. If you switch to a native video element, call `videoRef.current.pause()` inside the close handler before setting state.
Can I use this section without the stats row?
Yes. The stats block is conditionally rendered (`stats && stats.length > 0`), so passing an empty array or omitting the prop removes it entirely with no layout side effects.
Is the modal keyboard-accessible?
The close button is a native button element with an aria-label, so it receives focus and responds to Enter/Space. For full trap-focus behavior, add a focus trap library or manually move focus to the close button on open and restore it to the thumbnail on close.