LP Hero Video Skill
A guide for effectively integrating product demo videos into landing pages and websites.
Supported Patterns
- Hero Background Video — Full-screen background video at the top of the page
- Product Demo Section — Inline video in a product introduction section
- Floating Device Mockup — Display video inside a device frame
- Lightbox / Modal Video — Click to expand and play
Step 0: Pre-Implementation Checklist
Before adding a video, verify the following:
Video Asset Check
- Is the video file ready? (If not, create a placeholder UI first)
- Formats: Ideally both
.mp4(H.264) and.webm(VP9) - Resolution: 1920x1080 recommended for Hero, 1280x720 sufficient for card videos
- File size: 5-15 MB for Hero, 2-5 MB for card videos
- Duration: 10-30 seconds loop best for autoplay
Creating Video Assets (e.g., recording with Screen Studio)
- Record the most impressive product workflow in 15-30 seconds
- Record at 1920x1080 or higher resolution
- Set the following during export:
- MP4 (H.264): Quality Medium-High, bitrate 4-8 Mbps
- WebM (VP9): Export additional copy at equivalent quality (if supported by the tool)
- ffmpeg conversion commands:
# MP4 optimization (Web-ready, with faststart)
ffmpeg -i input.mov -c:v libx264 -crf 23 -preset slow \
-movflags +faststart -an -vf scale=1920:-2 output.mp4
# WebM conversion
ffmpeg -i input.mov -c:v libvpx-vp9 -crf 30 -b:v 0 \
-an -vf scale=1920:-2 output.webm
# For smaller card videos
ffmpeg -i input.mov -c:v libx264 -crf 26 -preset slow \
-movflags +faststart -an -vf scale=1280:-2 output-card.mp4
Hosting
- Local (
/public/videos/): For small sites, Vercel / Netlify deployments - CDN / Object Storage: For large scale, multiple videos (Cloudflare R2, AWS S3 + CloudFront)
- YouTube / Vimeo Embed: When SEO is a priority, or you want to share externally
Pattern 1: Hero Background Video
Display a full-screen product demo video at the top of the page, with a text overlay.
React Component
interface HeroVideoProps {
/** Path to the MP4 video */
videoSrc: string;
/** Path to the WebM video (optional) */
videoSrcWebm?: string;
/** Poster image to display before the video loads */
posterSrc?: string;
/** Child elements for overlay (e.g., heading) */
children?: React.ReactNode;
/** Overlay darkness (0-1, default 0.4) */
overlayOpacity?: number;
}
function HeroVideo({
videoSrc,
videoSrcWebm,
posterSrc,
children,
overlayOpacity = 0.4,
}: HeroVideoProps) {
const videoRef = useRef<HTMLVideoElement>(null);
return (
<div className="relative h-screen w-full overflow-hidden bg-black">
{/* Background Video */}
<video
ref={videoRef}
autoPlay
muted
loop
playsInline
poster={posterSrc}
className="absolute inset-0 w-full h-full object-cover"
>
{videoSrcWebm && <source src={videoSrcWebm} type="video/webm" />}
<source src={videoSrc} type="video/mp4" />
</video>
{/* Dark Overlay */}
<div
className="absolute inset-0"
style={{ backgroundColor: `rgba(0, 0, 0, ${overlayOpacity})` }}
/>
{/* Gradient fade to page background */}
<div className="absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-background to-transparent" />
{/* Content */}
{children && (
<div className="absolute inset-0 z-10 flex items-end">
<div className="container pb-16 md:pb-24">{children}</div>
</div>
)}
</div>
);
}
Usage Example
<HeroVideo
videoSrc="/videos/product-demo.mp4"
videoSrcWebm="/videos/product-demo.webm"
posterSrc="/images/hero-poster.jpg"
overlayOpacity={0.35}
>
<h1 className="text-4xl md:text-6xl font-normal text-white tracking-tight">
Reinventing conversation with technology.
</h1>
</HeroVideo>
Application Point on Plural Reality LP
Replace or co-exist with the current LifeGameHero component:
// Home.tsx replacement pattern
// Option A: Full replacement
<HeroVideo videoSrc="/videos/hero-demo.mp4" posterSrc="/images/IMG_5298.jpeg">
<h1 ...>{h.heroTitle}</h1>
</HeroVideo>
// Option B: Keep LifeGameHero and add a video section below
<LifeGameHero>...</LifeGameHero>
<Section className="py-0">
<HeroVideo videoSrc="/videos/product-overview.mp4" overlayOpacity={0.2}>
<h2>Product Introduction</h2>
</HeroVideo>
</Section>
Pattern 2: Product Card Video
Replace static images with demo videos on product cards, playing on hover.
React Component
interface ProductCardVideoProps {
videoSrc: string;
posterSrc: string;
alt: string;
className?: string;
}
function ProductCardVideo({
videoSrc,
posterSrc,
alt,
className,
}: ProductCardVideoProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const handleMouseEnter = () => {
videoRef.current?.play();
};
const handleMouseLeave = () => {
const video = videoRef.current;
if (video) {
video.pause();
video.currentTime = 0;
}
};
return (
<div
className={cn("relative aspect-video bg-muted overflow-hidden", className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<video
ref={videoRef}
muted
loop
playsInline
poster={posterSrc}
preload="metadata"
className="w-full h-full object-cover"
>
<source src={videoSrc} type="video/mp4" />
</video>
</div>
);
}
Usage Example (Replacing existing card image)
// Before:
<ParallaxImage src="/images/kouchoai.png" alt={t.products.kouchouAI} />
// After:
<ProductCardVideo
videoSrc="/videos/kouchou-ai-demo.mp4"
posterSrc="/images/kouchoai.png"
alt={t.products.kouchouAI}
/>
Pattern 3: Device Mockup Video
Display demo videos inside browser or smartphone frames (increases credibility).
function BrowserMockupVideo({
videoSrc,
posterSrc,
title,
}: {
videoSrc: string;
posterSrc?: string;
title?: string;
}) {
return (
<div className="border border-border bg-background overflow-hidden">
{/* Browser chrome */}
<div className="flex items-center gap-2 px-4 py-3 bg-secondary/30 border-b border-border">
<div className="flex gap-1.5">
<div className="w-3 h-3 rounded-full bg-muted-foreground/20" />
<div className="w-3 h-3 rounded-full bg-muted-foreground/20" />
<div className="w-3 h-3 rounded-full bg-muted-foreground/20" />
</div>
{title && (
<div className="flex-1 text-center">
<span className="text-xs text-muted-foreground font-mono">{title}</span>
</div>
)}
</div>
{/* Video content */}
<div className="aspect-video">
<video
autoPlay
muted
loop
playsInline
poster={posterSrc}
className="w-full h-full object-cover"
>
<source src={videoSrc} type="video/mp4" />
</video>
</div>
</div>
);
}
Pattern 4: Lightbox Modal Video
Display the video in a modal after clicking a thumbnail. Supports YouTube embeds.
function VideoLightbox({
thumbnailSrc,
videoSrc,
youtubeId,
alt,
}: {
thumbnailSrc: string;
videoSrc?: string;
youtubeId?: string;
alt: string;
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button
onClick={() => setIsOpen(true)}
className="relative group cursor-pointer aspect-video w-full overflow-hidden"
>
<img src={thumbnailSrc} alt={alt} className="w-full h-full object-cover" />
{/* Play button overlay */}
<div className="absolute inset-0 flex items-center justify-center bg-black/20 group-hover:bg-black/30 transition-colors">
<div className="w-16 h-16 flex items-center justify-center bg-white/90 group-hover:bg-white transition-colors">
<svg className="w-6 h-6 text-foreground ml-1" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
</div>
</div>
</button>
{/* Modal */}
{isOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80"
onClick={() => setIsOpen(false)}
>
<div className="w-full max-w-5xl aspect-video" onClick={(e) => e.stopPropagation()}>
{youtubeId ? (
<iframe
src={`https://www.youtube.com/embed/${youtubeId}?autoplay=1`}
className="w-full h-full"
allow="autoplay; encrypted-media"
allowFullScreen
/>
) : (
<video
src={videoSrc}
autoPlay
controls
className="w-full h-full object-contain bg-black"
/>
)}
</div>
</div>
)}
</>
);
}
Implementation Checklist
When adding a video, check the following:
Performance
- Set the
autoPlay,muted,loop,playsInlineattributes (mandatory for iOS compatibility) - Specify a
posterattribute for an initial frame image (improves loading experience) - Use
preload="metadata"to keep initial loading light for card videos - Apply
-movflags +faststartto video files (progressive playback) - Provide both MP4 and WebM formats via
<source>(browser compatibility)
Responsive
- Use
object-cover/object-containfor proper fitting - Consider showing a static image instead of a video on mobile devices
const isMobile = window.innerWidth < 768; // On mobile, load only the poster image, not the video
Accessibility
- Autoplay videos must include
muted(audio playback without user interaction is blocked) - Respect
prefers-reduced-motionand fall back to a static image when animations are disabled@media (prefers-reduced-motion: reduce) { video { display: none; } .video-poster-fallback { display: block; } }
Design (Plural Reality Compliant)
- No rounded corners (
border-radius: 0px) - Border:
1px solid var(--border) - Overlays are black gradients (maintaining monochromatic scheme)
- Use Lucide React's
Playicon for the play button
File Organization Convention
client/
public/
videos/
hero-demo.mp4 # Hero video (MP4)
hero-demo.webm # Hero video (WebM)
hero-poster.jpg # Hero video poster image
kouchou-ai-demo.mp4 # Kouchou AI demo video
baisoku-survey-demo.mp4 # Baisoku Survey demo video
baisoku-kaigi-demo.mp4 # Baisoku Kaigi demo video
src/
components/
HeroVideo.tsx # Hero full-screen video component
ProductCardVideo.tsx # Card hover-to-play component
BrowserMockupVideo.tsx # Browser-framed video
VideoLightbox.tsx # Modal video component
Common Layout Examples
SaaS / Product LP (Recommended Layout)
Hero full-screen video (15-30 second loop, muted autoplay)
↓
Text section (Mission & Values)
↓
Browser mockup video (main product demo)
↓
Product cards (hover to play individual demo videos)
↓
Case studies / Testimonials
↓
CTA
Corporate Website (Subtle Layout)
Hero image + play button (click to open Lightbox video)
↓
Text section
↓
Product cards (static images, video on detail page)
Pattern 5: CSS Animation Product UI Mockup (Recommended)
Render the product's operational feel directly with CSS animations / SVG / Canvas, without video files. This is the approach adopted by Stripe, Linear, and Vercel.
Benefits
- No video files required (zero byte addition)
- Fully responsive (works on any screen size)
- Native rendering for sharpness (no compression artifacts)
- Supports interactivity (hover, click for changes)
- Automatically adapts to dark/light mode
Prerequisites: Existing Animation Assets
The Plural Reality LP already has the following high-quality animations implemented in SolutionAnimations.tsx:
| Component | Description | Technology |
|---|---|---|
NetworkOntologyAnimation | 3-layer ontology (DATA→ONTOLOGY→APPLICATION) | SVG + framer-motion |
DataFlowAnimation | Collect→Organize→Connect pipeline | SVG + framer-motion |
ClusterAnimation | Opinion clustering visualization | SVG + framer-motion |
ConversationBranchAnimation | In-depth interview branch tree | SVG + framer-motion |
FeedbackLoopAnimation | Continuous dialogue cycle | SVG + framer-motion |
IntegrationMeshAnimation | Platform integration diagram | SVG + framer-motion |
ParticleFieldAnimation | Particle network | Canvas |
IsometricLayersAnimation | Isometric projection layer structure | SVG + framer-motion |
Also, LifeGameHero.tsx has a Canvas animation from Earth to Japan.
Common Design Principles
- Background: Black (#000000), drawn with white elements
- Monochromatic (no color, or only Brand Teal as accent)
- Font: var(--font-mono) for tech labels, var(--font-sans) for UI elements
- No rounded corners: rx={0} / border-radius: 0px
- Animation: framer-motion's useInView + motion.* for scroll-triggered animation
- Grid background: Subtle white line grid pattern (GridBg utility)
- Glow: feDropShadow for subtle glow (GlowDef utility)
Pattern 5A: Browser Frame + Animated UI
Display an animated mock UI inside a browser chrome (title bar + dots).
function ProductDemoMock({
title,
children,
className,
}: {
title: string;
children: React.ReactNode;
className?: string;
}) {
return (
<div className={cn("border border-border bg-black overflow-hidden", className)}>
{/* Browser chrome */}
<div className="flex items-center gap-2 px-4 py-3 bg-[#1c1c1e] border-b border-border">
<div className="flex gap-1.5">
<div className="w-2.5 h-2.5 rounded-full bg-white/10" />
<div className="w-2.5 h-2.5 rounded-full bg-white/10" />
<div className="w-2.5 h-2.5 rounded-full bg-white/10" />
</div>
<div className="flex-1 text-center">
<span className="text-[10px] text-white/40 font-mono">{title}</span>
</div>
</div>
{/* Animated content */}
<div className="aspect-video relative overflow-hidden">
{children}
</div>
</div>
);
}
Pattern 5B: Kouchou AI Demo Animation
An animation showing text data flowing in → AI clustering → results being displayed.
// Conceptual design (refer to ClusterAnimation in SolutionAnimations for implementation)
function KouchouAIDemoAnimation() {
const [phase, setPhase] = useState<"input" | "processing" | "result">("input");
useEffect(() => {
const timers = [
setTimeout(() => setPhase("processing"), 2000),
setTimeout(() => setPhase("result"), 4000),
];
const loop = setInterval(() => {
setPhase("input");
timers.push(setTimeout(() => setPhase("processing"), 2000));
timers.push(setTimeout(() => setPhase("result"), 4000));
}, 8000);
return () => { timers.forEach(clearTimeout); clearInterval(loop); };
}, []);
return (
<ProductDemoMock title="kouchou-ai.plural-reality.com">
<svg viewBox="0 0 800 450" className="w-full h-full">
{/* Phase 1: Text lines slide in from the left */}
{phase === "input" && (
<g>
{["Traffic is inconvenient", "I want better childcare support", "I'm worried about disaster measures"].map((text, i) => (
<motion.g
key={text}
initial={{ opacity: 0, x: -40 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.4 }}
>
<rect x={40} y={60 + i * 50} width={350} height={36} fill="white" fillOpacity={0.05} stroke="white" strokeWidth={0.5} />
<text x={56} y={82 + i * 50} fill="white" fontSize={11} fontFamily="var(--font-sans)" opacity={0.8}>
{text}
</text>
</motion.g>
))}
</g>
)}
{/* Phase 2: Particles converge to the center */}
{phase === "processing" && (
<motion.text x={400} y={225} textAnchor="middle" fill="white" fontSize={12} fontFamily="var(--font-mono)"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
Processing...
</motion.text>
)}
{/* Phase 3: Clustering result display */}
{phase === "result" && (
<g>
<motion.circle cx={200} cy={150} r={60} fill="none" stroke="white" strokeWidth={0.5}
initial={{ r: 0 }}
animate={{ r: 60 }}
transition={{ duration: 0.8 }}
/>
<motion.text x={200} y={155} textAnchor="middle" fill="white" fontSize={10} fontFamily="var(--font-sans)">
Urban Development
</motion.text>
<motion.circle cx={500} cy={180} r={50} fill="none" stroke="white" strokeWidth={0.5}
initial={{ r: 0 }}
animate={{ r: 50 }}
transition={{ duration: 0.8, delay: 0.3 }}
/>
<motion.text x={500} y={185} textAnchor="middle" fill="white" fontSize={10} fontFamily="var(--font-sans)">
Childcare & Education
</motion.text>
<motion.circle cx={350} cy={320} r={55} fill="none" stroke="white" strokeWidth={0.5}
initial={{ r: 0 }}
animate={{ r: 55 }}
transition={{ duration: 0.8, delay: 0.6 }}
/>
<motion.text x={350} y={325} textAnchor="middle" fill="white" fontSize={10} fontFamily="var(--font-sans)">
Disaster Prevention
</motion.text>
</g>
)}
</svg>
</ProductDemoMock>
);
}
Pattern 5C: Baisoku Survey Demo (Animated Form)
function BaisokuSurveyDemoAnimation() {
return (
<ProductDemoMock title="baisoku-survey.plural-reality.com">
<svg viewBox="0 0 800 450" className="w-full h-full">
{/* Animated fill bar */}
<motion.rect
x={50} y={50} width={700} height={20}
fill="white" fillOpacity={0.1}
rx={0}
/>
<motion.rect
x={50} y={50} height={20}
fill="white" fillOpacity={0.3}
initial={{ width: 0 }}
animate={{ width: 700 }}
transition={{ duration: 3, ease: "easeInOut", repeat: Infinity, repeatDelay: 1 }}
rx={0}
/>
{/* Question text appearing one by one */}
{["Q1: Age group?", "Q2: Occupation?", "Q3: Needs?"].map((q, i) => (
<motion.text
key={q}
x={70}
y={120 + i * 60}
fill="white"
fontSize={14}
fontFamily="var(--font-sans)"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 1.2, duration: 0.5, repeat: Infinity, repeatDelay: 4 - i * 1.2 }}
>
{q}
</motion.text>
))}
{/* Progress percentage */}
<motion.text
x={730} y={64} textAnchor="end" fill="white" fontSize={11} fontFamily="var(--font-mono)"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, repeat: Infinity, repeatDelay: 4 }}
>
100%
</motion.text>
</svg>
</ProductDemoMock>
);
}
Pattern 5D: Baisoku Kaigi Demo (Dialog Box Animation)
function BaisokuKaigiDemoAnimation() {
return (
<ProductDemoMock title="baisoku-kaigi.plural-reality.com">
<svg viewBox="0 0 800 450" className="w-full h-full">
{/* Person A's speech bubble */}
<motion.g
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, repeat: Infinity, repeatDelay: 5 }}
>
<rect x={40} y={60} width={280} height={50} fill="white" fillOpacity={0.1} stroke="white" strokeWidth={0.5} rx={0} />
<text x={56} y={90} fill="white" fontSize={13} fontFamily="var(--font-sans)">
Should we allocate more budget?
</text>
</motion.g>
{/* Person B's speech bubble */}
<motion.g
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 1.5, repeat: Infinity, repeatDelay: 4.5 }}
>
<rect x={480} y={160} width={280} height={50} fill="white" fillOpacity={0.1} stroke="white" strokeWidth={0.5} rx={0} />
<text x={496} y={190} fill="white" fontSize={13} fontFamily="var(--font-sans)">
Let's check the survey results.
</text>
</motion.g>
{/* AI summary displayed on the left */}
<motion.g
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 3, repeat: Infinity, repeatDelay: 3 }}
>
<rect x={40} y={280} width={280} height={80} fill="white" fillOpacity={0.05} stroke="white" strokeWidth={0.5} strokeDasharray="3 3" rx={0} />
<text x={56} y={300} fill="white" fontSize={10} fontFamily="var(--font-mono)" opacity={0.6}>
AI Analysis
</text>
<text x={56} y={320} fill="white" fontSize={11} fontFamily="var(--font-sans)">
72% of opinions support the budget increase.
</text>
<text x={56} y={340} fill="white" fontSize={11} fontFamily="var(--font-sans)">
Key concern: impact on other projects.
</text>
</motion.g>
</svg>
</ProductDemoMock>
);
}
Summary of Best Practices
- Start with an animation mockup (Pattern 5) and switch to video only when you need specific footage
- Provide multiple formats — MP4 (H.264) + WebM (VP9) for maximum compatibility
- Optimize heavily — Compress videos with ffmpeg and add
-movflags +faststart - Do not autoplay audio — Always set
mutedattribute - Always set a poster — A static image to show before the video loads
- Respect reduced motion — Provide a fallback for users who prefer reduced animation
- Handle mobile device constraints — Consider conditionally loading static images instead of videos on less capable devices