Web InnoventixFreeCode

Video Bg Hero

Original · free

hero over a looping gradient/canvas backdrop

byWeb InnoventixReact + Tailwind
heroxvideobgheroes
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/herox-video-bg.json
herox-video-bg.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { motion, useReducedMotion, type Variants } from "motion/react";

type PaletteKey = "aurora" | "ember" | "tidal";

type Blob = {
  cx: number;
  cy: number;
  ax: number;
  ay: number;
  sx: number;
  sy: number;
  r: number;
  ci: number;
  ph: number;
};

const PALETTES: Record<PaletteKey, { label: string; colors: string[] }> = {
  aurora: { label: "Aurora", colors: ["#6366f1", "#8b5cf6", "#22d3ee", "#10b981"] },
  ember: { label: "Ember", colors: ["#f43f5e", "#f59e0b", "#a855f7", "#fb7185"] },
  tidal: { label: "Tidal", colors: ["#0ea5e9", "#6366f1", "#14b8a6", "#38bdf8"] },
};

const PALETTE_ORDER: PaletteKey[] = ["aurora", "ember", "tidal"];

const BLOBS: Blob[] = [
  { cx: 0.22, cy: 0.28, ax: 0.1, ay: 0.09, sx: 0.34, sy: 0.28, r: 0.85, ci: 0, ph: 0 },
  { cx: 0.78, cy: 0.3, ax: 0.12, ay: 0.08, sx: 0.27, sy: 0.39, r: 0.95, ci: 1, ph: 1.2 },
  { cx: 0.6, cy: 0.74, ax: 0.14, ay: 0.1, sx: 0.31, sy: 0.24, r: 0.9, ci: 2, ph: 2.4 },
  { cx: 0.34, cy: 0.68, ax: 0.11, ay: 0.12, sx: 0.22, sy: 0.33, r: 0.8, ci: 3, ph: 3.6 },
  { cx: 0.5, cy: 0.46, ax: 0.16, ay: 0.1, sx: 0.19, sy: 0.29, r: 0.7, ci: 0, ph: 4.8 },
];

const EASE: [number, number, number, number] = [0.22, 1, 0.36, 1];

const STATS: { value: string; label: string }[] = [
  { value: "60fps", label: "on mid-range laptops" },
  { value: "11 KB", label: "gzipped, only React" },
  { value: "0", label: "video files to host" },
];

function withAlpha(hex: string, a: number): string {
  const n = parseInt(hex.slice(1), 16);
  const r = (n >> 16) & 255;
  const g = (n >> 8) & 255;
  const b = n & 255;
  return `rgba(${r}, ${g}, ${b}, ${a})`;
}

const container: Variants = {
  hidden: {},
  show: { transition: { staggerChildren: 0.09, delayChildren: 0.12 } },
};

const item: Variants = {
  hidden: { opacity: 0, y: 22 },
  show: { opacity: 1, y: 0, transition: { duration: 0.65, ease: EASE } },
};

export default function HeroxVideoBg() {
  const reduce = useReducedMotion() ?? false;
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const [palette, setPalette] = useState<PaletteKey>("aurora");
  const [playing, setPlaying] = useState(true);
  const [isDark, setIsDark] = useState(false);

  const running = playing && !reduce;

  useEffect(() => {
    const mql = window.matchMedia("(prefers-color-scheme: dark)");
    const root = document.documentElement;
    const compute = () => setIsDark(root.classList.contains("dark") || mql.matches);
    compute();
    mql.addEventListener("change", compute);
    const obs = new MutationObserver(compute);
    obs.observe(root, { attributes: true, attributeFilter: ["class"] });
    return () => {
      mql.removeEventListener("change", compute);
      obs.disconnect();
    };
  }, []);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    let width = 0;
    let height = 0;
    let raf = 0;
    const animate = playing && !reduce;

    const drawFrame = (t: number) => {
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.globalCompositeOperation = "source-over";
      const bg = ctx.createLinearGradient(0, 0, width, height);
      if (isDark) {
        bg.addColorStop(0, "#070b18");
        bg.addColorStop(1, "#02040a");
      } else {
        bg.addColorStop(0, "#f8fafc");
        bg.addColorStop(1, "#eef2ff");
      }
      ctx.fillStyle = bg;
      ctx.fillRect(0, 0, width, height);

      ctx.globalCompositeOperation = isDark ? "lighter" : "source-over";
      const colors = PALETTES[palette].colors;
      const alpha = isDark ? 0.55 : 0.5;
      const base = Math.min(width, height);
      for (const b of BLOBS) {
        const x = (b.cx + Math.sin(t * b.sx + b.ph) * b.ax) * width;
        const y = (b.cy + Math.cos(t * b.sy + b.ph) * b.ay) * height;
        const r = b.r * base;
        const color = colors[b.ci] ?? "#6366f1";
        const g = ctx.createRadialGradient(x, y, 0, x, y, r);
        g.addColorStop(0, withAlpha(color, alpha));
        g.addColorStop(0.55, withAlpha(color, alpha * 0.35));
        g.addColorStop(1, withAlpha(color, 0));
        ctx.fillStyle = g;
        ctx.beginPath();
        ctx.arc(x, y, r, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.globalCompositeOperation = "source-over";
    };

    const resize = () => {
      const rect = canvas.getBoundingClientRect();
      width = rect.width;
      height = rect.height;
      canvas.width = Math.max(1, Math.floor(width * dpr));
      canvas.height = Math.max(1, Math.floor(height * dpr));
      if (!animate) drawFrame(6);
    };
    resize();
    window.addEventListener("resize", resize);

    const startT = performance.now();
    const loop = (now: number) => {
      drawFrame((now - startT) / 1000);
      raf = requestAnimationFrame(loop);
    };

    if (animate) {
      raf = requestAnimationFrame(loop);
    } else {
      drawFrame(6);
    }

    return () => {
      window.removeEventListener("resize", resize);
      if (raf) cancelAnimationFrame(raf);
    };
  }, [palette, isDark, playing, reduce]);

  return (
    <section className="relative flex w-full items-center overflow-hidden bg-white px-6 py-24 sm:py-32 dark:bg-slate-950 min-h-[92vh]">
      <style>{`
        @keyframes hxvb-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
        @keyframes hxvb-ring { 0% { transform: scale(1); opacity: .7; } 75%, 100% { transform: scale(2.4); opacity: 0; } }
        @keyframes hxvb-float { 0%, 100% { transform: translateY(0) rotate(6deg); } 50% { transform: translateY(-12px) rotate(6deg); } }
        @keyframes hxvb-pulse { 0%, 100% { opacity: .35; } 50% { opacity: .7; } }
        .hxvb-badge-glow { animation: hxvb-shimmer 7s linear infinite; }
        .hxvb-ring { animation: hxvb-ring 2.6s cubic-bezier(0,0,0.2,1) infinite; }
        .hxvb-float { animation: hxvb-float 9s ease-in-out infinite; }
        .hxvb-pulse { animation: hxvb-pulse 6s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .hxvb-badge-glow, .hxvb-ring, .hxvb-float, .hxvb-pulse { animation: none !important; }
        }
      `}</style>

      <canvas
        ref={canvasRef}
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 h-full w-full"
      />

      {/* legibility scrim */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 bg-gradient-to-b from-white/50 via-transparent to-white/70 dark:from-slate-950/55 dark:via-slate-950/10 dark:to-slate-950/85"
      />
      {/* soft center glow */}
      <div
        aria-hidden="true"
        className="hxvb-pulse pointer-events-none absolute left-1/2 top-1/2 h-[36rem] w-[36rem] -translate-x-1/2 -translate-y-1/2 rounded-full bg-indigo-400/10 blur-3xl dark:bg-indigo-500/10"
      />
      {/* floating decorative chip */}
      <div
        aria-hidden="true"
        className="hxvb-float pointer-events-none absolute right-[8%] top-[16%] hidden h-16 w-16 rounded-2xl bg-gradient-to-br from-indigo-400/40 to-sky-400/30 ring-1 ring-white/30 backdrop-blur-sm md:block dark:from-indigo-500/30 dark:to-sky-500/20 dark:ring-white/10"
      />

      <div className="relative mx-auto w-full max-w-4xl text-center">
        <motion.div
          variants={container}
          initial={reduce ? false : "hidden"}
          animate={reduce ? false : "show"}
          className="flex flex-col items-center"
        >
          {/* eyebrow badge */}
          <motion.span
            variants={item}
            className="relative inline-flex items-center gap-2 overflow-hidden rounded-full border border-slate-300/70 bg-white/70 px-3.5 py-1.5 text-xs font-medium tracking-wide text-slate-700 shadow-sm backdrop-blur dark:border-white/15 dark:bg-white/5 dark:text-slate-200"
          >
            <span
              aria-hidden="true"
              className="hxvb-badge-glow absolute inset-0"
              style={{
                background:
                  "linear-gradient(110deg, transparent 35%, rgba(255,255,255,0.45) 50%, transparent 65%)",
                backgroundSize: "200% 100%",
              }}
            />
            <span className="relative flex h-2 w-2">
              <span
                aria-hidden="true"
                className="hxvb-ring absolute inline-flex h-full w-full rounded-full bg-emerald-400"
              />
              <span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
            </span>
            <span className="relative">Fluxwave 3.0 &middot; Live canvas backdrops</span>
          </motion.span>

          {/* headline */}
          <motion.h1
            variants={item}
            className="mt-7 text-balance text-4xl font-semibold leading-[1.05] tracking-tight text-slate-900 sm:text-6xl dark:text-white"
          >
            Ship a hero that{" "}
            <span className="bg-gradient-to-r from-indigo-500 via-violet-500 to-sky-500 bg-clip-text text-transparent dark:from-indigo-400 dark:via-violet-400 dark:to-sky-400">
              moves like video
            </span>
            , loads like text.
          </motion.h1>

          {/* subhead */}
          <motion.p
            variants={item}
            className="mt-6 max-w-2xl text-pretty text-base leading-relaxed text-slate-600 sm:text-lg dark:text-slate-300"
          >
            Fluxwave renders a smooth, GPU-light gradient backdrop straight onto a canvas &mdash; no
            MP4s to host, no layout shift, no jank. One component, crisp in light and dark, on every
            screen.
          </motion.p>

          {/* CTAs */}
          <motion.div
            variants={item}
            className="mt-9 flex flex-col items-center gap-3 sm:flex-row"
          >
            <a
              href="#start"
              className="group inline-flex items-center justify-center gap-2 rounded-xl bg-slate-900 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-slate-900/20 transition-transform hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-white dark:text-slate-900 dark:shadow-black/40 dark:focus-visible:ring-offset-slate-950"
            >
              Start free
              <svg
                aria-hidden="true"
                viewBox="0 0 24 24"
                className="h-4 w-4 transition-transform group-hover:translate-x-0.5"
                fill="none"
                stroke="currentColor"
                strokeWidth={2}
                strokeLinecap="round"
                strokeLinejoin="round"
              >
                <path d="M5 12h14M13 6l6 6-6 6" />
              </svg>
            </a>
            <a
              href="#demo"
              className="inline-flex items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white/60 px-6 py-3 text-sm font-semibold text-slate-800 backdrop-blur transition-colors hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-white/20 dark:bg-white/5 dark:text-white dark:hover:bg-white/10 dark:focus-visible:ring-offset-slate-950"
            >
              <svg aria-hidden="true" viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor">
                <path d="M8 5.14v13.72a1 1 0 0 0 1.5.86l11-6.86a1 1 0 0 0 0-1.72l-11-6.86A1 1 0 0 0 8 5.14Z" />
              </svg>
              See it live
            </a>
          </motion.div>

          {/* trust row */}
          <motion.div
            variants={item}
            className="mt-9 flex flex-col items-center gap-3 sm:flex-row sm:gap-5"
          >
            <div className="flex -space-x-2.5">
              {[
                "from-indigo-400 to-violet-500",
                "from-sky-400 to-emerald-500",
                "from-rose-400 to-amber-500",
                "from-violet-400 to-sky-500",
              ].map((g, i) => (
                <span
                  key={i}
                  className={`h-8 w-8 rounded-full bg-gradient-to-br ${g} ring-2 ring-white dark:ring-slate-950`}
                />
              ))}
            </div>
            <div className="flex items-center gap-2 text-sm text-slate-600 dark:text-slate-300">
              <span className="flex" aria-hidden="true">
                {[0, 1, 2, 3, 4].map((s) => (
                  <svg key={s} viewBox="0 0 24 24" className="h-4 w-4 text-amber-400" fill="currentColor">
                    <path d="m12 2 2.9 6.26L21.6 9.3l-4.8 4.68 1.13 6.6L12 17.5 6.07 20.6l1.13-6.6L2.4 9.3l6.7-1.04Z" />
                  </svg>
                ))}
              </span>
              <span>
                <strong className="font-semibold text-slate-900 dark:text-white">4.9/5</strong> from
                4,200+ product teams
              </span>
            </div>
          </motion.div>

          {/* stats */}
          <motion.dl
            variants={item}
            className="mt-12 grid w-full max-w-lg grid-cols-3 gap-4 border-t border-slate-200/70 pt-8 dark:border-white/10"
          >
            {STATS.map((s) => (
              <div key={s.label} className="text-center">
                <dt className="text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl dark:text-white">
                  {s.value}
                </dt>
                <dd className="mt-1 text-xs text-slate-500 dark:text-slate-400">{s.label}</dd>
              </div>
            ))}
          </motion.dl>

          {/* backdrop controls */}
          <motion.div
            variants={item}
            className="mt-10 inline-flex flex-wrap items-center justify-center gap-3 rounded-2xl border border-slate-200/70 bg-white/60 px-3 py-2.5 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5"
          >
            <span className="pl-1 text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400">
              Backdrop
            </span>
            <div
              role="group"
              aria-label="Backdrop palette"
              className="flex items-center gap-1 rounded-xl bg-slate-100/80 p-1 dark:bg-white/5"
            >
              {PALETTE_ORDER.map((key) => {
                const active = palette === key;
                return (
                  <button
                    key={key}
                    type="button"
                    aria-pressed={active}
                    onClick={() => setPalette(key)}
                    className={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 ${
                      active
                        ? "bg-slate-900 text-white shadow-sm dark:bg-white dark:text-slate-900"
                        : "text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white"
                    }`}
                  >
                    {PALETTES[key].label}
                  </button>
                );
              })}
            </div>
            <button
              type="button"
              onClick={() => setPlaying((p) => !p)}
              disabled={reduce}
              aria-pressed={running}
              aria-label={running ? "Pause backdrop animation" : "Play backdrop animation"}
              className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-slate-300 bg-white text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-40 dark:border-white/15 dark:bg-white/5 dark:text-slate-200 dark:hover:bg-white/10"
            >
              {running ? (
                <svg aria-hidden="true" viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor">
                  <path d="M8 5h3v14H8zM13 5h3v14h-3z" />
                </svg>
              ) : (
                <svg aria-hidden="true" viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor">
                  <path d="M8 5.14v13.72a1 1 0 0 0 1.5.86l11-6.86a1 1 0 0 0 0-1.72l-11-6.86A1 1 0 0 0 8 5.14Z" />
                </svg>
              )}
            </button>
          </motion.div>
        </motion.div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →
Spotlight Hero

Spotlight Hero

Original

A centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero

Split Hero

Original

A two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero

Gradient Spotlight Hero

Original

A minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero

App Preview Hero

Original

A centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero

Waitlist Capture Hero

Original

A dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

Image Backdrop Hero

Image Backdrop Hero

Original

A cinematic full-bleed hero with a layered image-style backdrop, overlaid left-aligned copy and a trusted-by logos strip.

Gradient Mesh Hero with Staggered Text Reveal

Gradient Mesh Hero with Staggered Text Reveal

Original

A full-screen hero pairing a drifting animated gradient-mesh backdrop with a word-by-word staggered blur-in headline and a logo marquee under the fold.

Typewriter Rotating Words Hero

Typewriter Rotating Words Hero

Original

A hero whose headline cycles through rotating words with a live typewriter caret over an animated grid backdrop, degrading to a gentle word swap when reduced motion is on.

Floating UI Cards Hero with Mouse Parallax

Floating UI Cards Hero with Mouse Parallax

Original

A hero with glassy floating UI cards that drift continuously and shift on mouse-parallax depth around a staggered centre headline.

Aurora Hero with Shimmer CTA and Logo Marquee

Aurora Hero with Shimmer CTA and Logo Marquee

Original

A dark hero with animated aurora light beams, a shimmering sweep across the primary CTA, and dual-direction logo marquees beneath the fold.

Gradient Conversion Hero

Gradient Conversion Hero

MIT

Centered, full-screen marketing hero with a colour-accent headline word, supporting copy, and dual primary/secondary CTA buttons. Dark-mode ready and dependency-free.

Split Showcase Hero

Split Showcase Hero

MIT

Two-column hero on a dark background with a headline, paragraph, and two buttons beside a gradient media panel. Responsive column stack on mobile.