proNew426 lines · framer-motion
Video Showcase Modal
Press play on a demo that was never recorded. A product-demo section with a generated poster card and play button that opens a custom modal playing an entirely generated abstract 'video' — drawn live on canvas, with real play/pause and a progress bar.
- Use it for
- The feature or product section of a SaaS site that wants a 'watch the demo' moment without shipping a real video file or an iframe embed.
- Themes
- Light + dark
- License
- Commercial use
Live
Product demo
See it move before you ship it
A full walkthrough of the product, from first login to the moment it pays off.
tsx
"use client";
import * as React from "react";
import { createPortal } from "react-dom";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils";
function useReducedMotionSafe() {
const prefersReduced = useReducedMotion();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
return mounted ? !!prefersReduced : false;
}
/**
* Full-width product-demo section: a generated poster card with a play button opens a
* custom modal (portaled to document.body) containing an entirely generated "video" —
* a canvas loop of drifting abstract shapes, with real play/pause and an elapsed-time
* progress bar. No <video> tag, no external media, no native dialog/confirm.
*/
export interface VideoShowcaseModalProps {The rest is in Pro.
Full source, the rebuild prompt and registry install unlock with Pro or Lifetime.
Get ProHow it behaves
- →Poster frame is a static generated CSS/SVG composition (gradient + traced circles) with a centred play button; clicking it opens a modal rendered via createPortal to document.body.
- →The modal's canvas draws one of three generated motion patterns (flowing blurred blobs, orbiting rings, or drifting particles) on a requestAnimationFrame loop; colours are read from --signal/--info/--lime/--ok and re-read on every class change on <html> via MutationObserver, so both themes look correct.
- →A real play/pause button toggles a ref-backed flag that gates whether elapsed time advances in the raf loop; the canvas keeps rendering the current (frozen) frame while paused instead of stopping outright.
- →A thin progress bar fills as the generated loop plays and resets every duration seconds; it's driven by accumulated performance.now() deltas inside the effect, never by Date.now() in render.
- →Escape, a backdrop click, or the close button all close the modal; the raf loop, ResizeObserver, IntersectionObserver and MutationObserver are torn down on close and on unmount.
- →An IntersectionObserver on the video host pauses time advancement when it scrolls offscreen, and document.hidden pauses it when the tab isn't visible.
- →prefers-reduced-motion renders one static generated frame in the modal instead of animating.
Accessibility
- ✓The poster is a real <button aria-haspopup="dialog"> with a visible focus ring, not a clickable div.
- ✓The modal has role="dialog" and aria-modal="true"; focus moves to the close button on open and Escape closes it.
- ✓Play/pause button exposes aria-pressed and an accurate aria-label ("Play" / "Pause"); the progress bar is a role="progressbar" with aria-valuemin/aria-valuemax.
- ✓All decorative canvas and SVG layers are aria-hidden so only the real controls reach the accessibility tree.
- ✓Honours prefers-reduced-motion.