Communitygithub.com

yuidvg/lp-hero-video

A skill for integrating product demo videos into landing pages. Implements demo presentation patterns for websites, including CSS-animated product UI mockups, hero videos, and card-based videos.

What is lp-hero-video?

This skill, `yuidvg/lp-hero-video`, provides comprehensive guidance and reusable React components for incorporating product demo videos into landing pages and websites. It addresses common challenges like video optimization, responsive design, and performance, offering patterns for hero background videos, hover-triggered product card videos, device mockups, and lightbox modals. The skill emphasizes high-quality, compressed video formats (MP4 for H.264 and WebM for VP9) and provides ffmpeg commands for optimal web delivery. It includes a detailed implementation checklist covering performance (with attributes like `autoPlay muted loop playsInline iOS`), responsive breakpoints, accessibility (including the `prefers-reduced-motion` fallback), and design guidelines. Beyond real video, it introduces reactive CSS animation-based UI mockups as a zero-byte alternative, leveraging existing SVG and framer-motion assets for a sharp, responsive experience. Designed for developers using React-based meta-frameworks (like Next.js or similar), this skill bridges the gap between static product images and full video production, enabling polished, performant demos that enhance user engagement on landing pages.

Works with~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/yuidvg/skills/tree/HEAD/skills/lp-hero-video

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

LP Hero Video Skill

A guide for effectively integrating product demo videos into landing pages and websites.

Supported Patterns

  1. Hero Background Video — Full-screen background video at the top of the page
  2. Product Demo Section — Inline video in a product introduction section
  3. Floating Device Mockup — Display video inside a device frame
  4. 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)

  1. Record the most impressive product workflow in 15-30 seconds
  2. Record at 1920x1080 or higher resolution
  3. 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)
  4. 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, playsInline attributes (mandatory for iOS compatibility)
  • Specify a poster attribute for an initial frame image (improves loading experience)
  • Use preload="metadata" to keep initial loading light for card videos
  • Apply -movflags +faststart to video files (progressive playback)
  • Provide both MP4 and WebM formats via <source> (browser compatibility)

Responsive

  • Use object-cover / object-contain for 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-motion and 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 Play icon 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:

ComponentDescriptionTechnology
NetworkOntologyAnimation3-layer ontology (DATA→ONTOLOGY→APPLICATION)SVG + framer-motion
DataFlowAnimationCollect→Organize→Connect pipelineSVG + framer-motion
ClusterAnimationOpinion clustering visualizationSVG + framer-motion
ConversationBranchAnimationIn-depth interview branch treeSVG + framer-motion
FeedbackLoopAnimationContinuous dialogue cycleSVG + framer-motion
IntegrationMeshAnimationPlatform integration diagramSVG + framer-motion
ParticleFieldAnimationParticle networkCanvas
IsometricLayersAnimationIsometric projection layer structureSVG + 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

  1. Start with an animation mockup (Pattern 5) and switch to video only when you need specific footage
  2. Provide multiple formats — MP4 (H.264) + WebM (VP9) for maximum compatibility
  3. Optimize heavily — Compress videos with ffmpeg and add -movflags +faststart
  4. Do not autoplay audio — Always set muted attribute
  5. Always set a poster — A static image to show before the video loads
  6. Respect reduced motion — Provide a fallback for users who prefer reduced animation
  7. Handle mobile device constraints — Consider conditionally loading static images instead of videos on less capable devices

Related Skills