Web InnoventixFreeCode

Split Image Hero

Original · free

split hero with copy and image

byWeb InnoventixReact + Tailwind
heroxsplitimageheroes
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-split-image.json
herox-split-image.tsx
"use client";

import { useRef, useState } from "react";
import type { FormEvent, KeyboardEvent } from "react";
import { motion, useReducedMotion } from "motion/react";

const TABS = [
  { id: "overview", label: "Overview" },
  { id: "insights", label: "Insights" },
  { id: "reports", label: "Reports" },
] as const;

type TabId = (typeof TABS)[number]["id"];

const BAR_DATA: Record<TabId, number[]> = {
  overview: [46, 62, 51, 74, 66, 90, 78],
  insights: [34, 48, 42, 55, 70, 63, 88],
  reports: [58, 52, 66, 60, 73, 68, 86],
};

const DAYS = ["M", "T", "W", "T", "F", "S", "S"];

const KPIS: { label: string; value: string; delta: string; up: boolean }[] = [
  { label: "Active projects", value: "128", delta: "+12%", up: true },
  { label: "On-time delivery", value: "94%", delta: "+5.2%", up: true },
  { label: "Avg. cycle time", value: "3.1d", delta: "-0.8d", up: true },
];

const REPORTS: { name: string; when: string; status: "Ready" | "In review" | "Draft" }[] = [
  { name: "Q3 delivery review", when: "Updated 2h ago", status: "Ready" },
  { name: "Sprint 42 retrospective", when: "Updated today", status: "In review" },
  { name: "Ops capacity forecast", when: "Updated yesterday", status: "Draft" },
  { name: "Churn-risk digest", when: "Updated 3d ago", status: "Ready" },
];

function StarRow() {
  return (
    <div className="flex items-center gap-0.5" aria-hidden="true">
      {Array.from({ length: 5 }).map((_, i) => (
        <svg key={i} viewBox="0 0 20 20" className="h-4 w-4 fill-amber-400">
          <path d="M10 1.6l2.47 5.01 5.53.8-4 3.9.94 5.51L10 14.23l-4.94 2.6.94-5.5-4-3.9 5.53-.81L10 1.6z" />
        </svg>
      ))}
    </div>
  );
}

function StatusBadge({ status }: { status: "Ready" | "In review" | "Draft" }) {
  const map: Record<typeof status, string> = {
    Ready:
      "bg-emerald-500/10 text-emerald-700 ring-emerald-500/25 dark:text-emerald-300",
    "In review":
      "bg-amber-500/10 text-amber-700 ring-amber-500/25 dark:text-amber-300",
    Draft: "bg-slate-500/10 text-slate-600 ring-slate-500/25 dark:text-slate-300",
  };
  return (
    <span
      className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium ring-1 ${map[status]}`}
    >
      <span className="h-1.5 w-1.5 rounded-full bg-current" />
      {status}
    </span>
  );
}

export default function HeroxSplitImage() {
  const reduce = useReducedMotion();
  const [active, setActive] = useState<TabId>("overview");
  const [email, setEmail] = useState("");
  const [sent, setSent] = useState(false);
  const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);

  const container = {
    hidden: {},
    show: { transition: { staggerChildren: 0.08, delayChildren: 0.04 } },
  };
  const item = reduce
    ? { hidden: { opacity: 0 }, show: { opacity: 1 } }
    : {
        hidden: { opacity: 0, y: 18 },
        show: {
          opacity: 1,
          y: 0,
          transition: { duration: 0.6, ease: "easeOut" as const },
        },
      };

  function onTabKey(e: KeyboardEvent<HTMLButtonElement>) {
    const idx = TABS.findIndex((t) => t.id === active);
    let next = idx;
    if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (idx + 1) % TABS.length;
    else if (e.key === "ArrowLeft" || e.key === "ArrowUp")
      next = (idx - 1 + TABS.length) % TABS.length;
    else if (e.key === "Home") next = 0;
    else if (e.key === "End") next = TABS.length - 1;
    else return;
    e.preventDefault();
    setActive(TABS[next].id);
    tabRefs.current[next]?.focus();
  }

  function onSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    if (!email.trim()) return;
    setSent(true);
  }

  const bars = BAR_DATA[active];

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 via-white to-slate-50 px-6 py-24 text-slate-900 sm:px-8 lg:py-32 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 dark:text-slate-50">
      <style>{`
        @keyframes hxsi_float {
          0%, 100% { transform: translate3d(0,0,0) scale(1); }
          50% { transform: translate3d(14px,-22px,0) scale(1.06); }
        }
        @keyframes hxsi_floatAlt {
          0%, 100% { transform: translate3d(0,0,0) scale(1); }
          50% { transform: translate3d(-18px,16px,0) scale(1.08); }
        }
        @keyframes hxsi_shimmer {
          0% { background-position: 0% 50%; }
          100% { background-position: 200% 50%; }
        }
        @keyframes hxsi_pulse {
          0%, 100% { opacity: 1; transform: scale(1); }
          50% { opacity: .35; transform: scale(.72); }
        }
        @keyframes hxsi_gridDrift {
          from { transform: translateY(0); }
          to { transform: translateY(46px); }
        }
        @media (prefers-reduced-motion: reduce) {
          .hxsi-anim { animation: none !important; }
        }
      `}</style>

      {/* ambient background */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
        <div
          className="hxsi-anim absolute -left-24 -top-24 h-80 w-80 rounded-full bg-indigo-400/25 blur-3xl dark:bg-indigo-600/25"
          style={{ animation: "hxsi_float 9s ease-in-out infinite" }}
        />
        <div
          className="hxsi-anim absolute -right-16 top-1/3 h-96 w-96 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-700/20"
          style={{ animation: "hxsi_floatAlt 11s ease-in-out infinite" }}
        />
        <div
          className="hxsi-anim absolute bottom-0 left-1/3 h-72 w-72 rounded-full bg-emerald-300/20 blur-3xl dark:bg-emerald-600/15"
          style={{ animation: "hxsi_float 13s ease-in-out infinite" }}
        />
        <div
          className="hxsi-anim absolute inset-0 opacity-[0.35] dark:opacity-20"
          style={{
            backgroundImage:
              "linear-gradient(to right, rgba(100,116,139,.14) 1px, transparent 1px), linear-gradient(to bottom, rgba(100,116,139,.14) 1px, transparent 1px)",
            backgroundSize: "46px 46px",
            maskImage: "radial-gradient(ellipse 90% 70% at 50% 0%, #000 40%, transparent 100%)",
            WebkitMaskImage:
              "radial-gradient(ellipse 90% 70% at 50% 0%, #000 40%, transparent 100%)",
            animation: "hxsi_gridDrift 20s linear infinite alternate",
          }}
        />
      </div>

      <div className="relative mx-auto grid max-w-7xl items-center gap-14 lg:grid-cols-[1.05fr_1fr] lg:gap-10">
        {/* ── LEFT: copy ── */}
        <motion.div
          variants={container}
          initial="hidden"
          animate="show"
          className="max-w-xl"
        >
          <motion.div variants={item}>
            <span className="inline-flex items-center gap-2 rounded-full border border-indigo-500/20 bg-indigo-500/10 px-3.5 py-1.5 text-xs font-semibold uppercase tracking-wider text-indigo-700 dark:text-indigo-300">
              <span
                className="hxsi-anim h-1.5 w-1.5 rounded-full bg-emerald-500"
                style={{ animation: "hxsi_pulse 2s ease-in-out infinite" }}
              />
              New — realtime workspace signals
            </span>
          </motion.div>

          <motion.h1
            variants={item}
            className="mt-6 text-4xl font-semibold leading-[1.05] tracking-tight sm:text-5xl lg:text-6xl"
          >
            Ship decisions your whole team can{" "}
            <span
              className="hxsi-anim bg-clip-text text-transparent"
              style={{
                backgroundImage:
                  "linear-gradient(110deg, #6366f1, #8b5cf6, #10b981, #6366f1)",
                backgroundSize: "200% 100%",
                animation: "hxsi_shimmer 6s linear infinite",
              }}
            >
              stand behind
            </span>
            .
          </motion.h1>

          <motion.p
            variants={item}
            className="mt-6 text-lg leading-relaxed text-slate-600 dark:text-slate-300"
          >
            Meridian pulls every project, metric, and deadline into one live
            workspace — so the next move is obvious before the meeting even
            starts. No more stale spreadsheets, no more guessing.
          </motion.p>

          {/* form */}
          <motion.form
            variants={item}
            onSubmit={onSubmit}
            noValidate
            className="mt-9"
          >
            <div className="flex flex-col gap-3 sm:flex-row">
              <label htmlFor="hxsi-email" className="sr-only">
                Work email
              </label>
              <div className="relative flex-1">
                <svg
                  viewBox="0 0 24 24"
                  aria-hidden="true"
                  className="pointer-events-none absolute left-3.5 top-1/2 h-5 w-5 -translate-y-1/2 text-slate-400"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="1.8"
                >
                  <rect x="3" y="5" width="18" height="14" rx="2" />
                  <path d="m3 7 9 6 9-6" />
                </svg>
                <input
                  id="hxsi-email"
                  type="email"
                  required
                  value={email}
                  onChange={(e) => {
                    setEmail(e.target.value);
                    if (sent) setSent(false);
                  }}
                  placeholder="you@company.com"
                  className="w-full rounded-xl border border-slate-300 bg-white/80 py-3 pl-11 pr-4 text-base text-slate-900 shadow-sm outline-none ring-indigo-500/50 transition placeholder:text-slate-400 focus:border-indigo-500 focus:ring-4 dark:border-slate-700 dark:bg-slate-900/70 dark:text-slate-50 dark:placeholder:text-slate-500"
                />
              </div>
              <button
                type="submit"
                className="group inline-flex items-center justify-center gap-2 rounded-xl bg-indigo-600 px-6 py-3 text-base font-semibold text-white shadow-lg shadow-indigo-600/25 outline-none ring-indigo-500/50 transition hover:bg-indigo-500 focus-visible:ring-4 active:scale-[0.98] dark:bg-indigo-500 dark:hover:bg-indigo-400"
              >
                Start free trial
                <svg
                  viewBox="0 0 24 24"
                  aria-hidden="true"
                  className="h-4 w-4 transition-transform group-hover:translate-x-0.5"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="2.2"
                >
                  <path d="M5 12h14M13 6l6 6-6 6" />
                </svg>
              </button>
            </div>

            <p aria-live="polite" className="mt-3 min-h-[1.25rem] text-sm">
              {sent ? (
                <span className="inline-flex items-center gap-1.5 font-medium text-emerald-600 dark:text-emerald-400">
                  <svg
                    viewBox="0 0 24 24"
                    aria-hidden="true"
                    className="h-4 w-4"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2.4"
                  >
                    <path d="m5 13 4 4L19 7" />
                  </svg>
                  Thanks — check {email} for your workspace link.
                </span>
              ) : (
                <span className="text-slate-500 dark:text-slate-400">
                  Free for 14 days · No credit card · SOC 2 ready
                </span>
              )}
            </p>
          </motion.form>

          {/* social proof */}
          <motion.div
            variants={item}
            className="mt-9 flex flex-wrap items-center gap-x-5 gap-y-3"
          >
            <div className="flex -space-x-2.5" aria-hidden="true">
              {[
                "from-indigo-400 to-violet-500",
                "from-emerald-400 to-teal-500",
                "from-rose-400 to-orange-500",
                "from-sky-400 to-indigo-500",
              ].map((g, i) => (
                <div
                  key={i}
                  className={`h-9 w-9 rounded-full bg-gradient-to-br ${g} ring-2 ring-white dark:ring-slate-950`}
                />
              ))}
            </div>
            <div className="flex flex-col">
              <StarRow />
              <span className="text-sm text-slate-600 dark:text-slate-400">
                Trusted by <span className="font-semibold text-slate-900 dark:text-slate-100">4,200+</span> product and ops teams
              </span>
            </div>
          </motion.div>
        </motion.div>

        {/* ── RIGHT: product preview ── */}
        <motion.div
          initial={reduce ? { opacity: 0 } : { opacity: 0, y: 24, scale: 0.97 }}
          animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0, scale: 1 }}
          transition={{ duration: 0.7, ease: "easeOut", delay: 0.15 }}
          className="relative"
        >
          {/* glow */}
          <div
            aria-hidden="true"
            className="absolute -inset-4 -z-10 rounded-[2rem] bg-gradient-to-tr from-indigo-500/30 via-violet-500/20 to-emerald-400/20 blur-2xl"
          />

          <div className="overflow-hidden rounded-2xl border border-slate-200/80 bg-white/90 shadow-2xl shadow-slate-900/10 backdrop-blur-sm dark:border-slate-700/70 dark:bg-slate-900/80 dark:shadow-black/40">
            {/* window chrome */}
            <div className="flex items-center gap-2 border-b border-slate-200/70 bg-slate-50/80 px-4 py-3 dark:border-slate-700/60 dark:bg-slate-900/60">
              <span className="h-3 w-3 rounded-full bg-rose-400" />
              <span className="h-3 w-3 rounded-full bg-amber-400" />
              <span className="h-3 w-3 rounded-full bg-emerald-400" />
              <div className="ml-3 flex items-center gap-2 rounded-md bg-white px-3 py-1 text-xs text-slate-400 ring-1 ring-slate-200 dark:bg-slate-800 dark:text-slate-500 dark:ring-slate-700">
                <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
                  <rect x="4" y="8" width="16" height="12" rx="2" />
                  <path d="M8 8V6a4 4 0 0 1 8 0v2" />
                </svg>
                app.meridian.io/workspace
              </div>
            </div>

            {/* tabs */}
            <div className="px-5 pt-5">
              <div
                role="tablist"
                aria-label="Workspace views"
                className="inline-flex gap-1 rounded-lg bg-slate-100 p-1 dark:bg-slate-800"
              >
                {TABS.map((t, i) => {
                  const selected = active === t.id;
                  return (
                    <button
                      key={t.id}
                      ref={(el) => {
                        tabRefs.current[i] = el;
                      }}
                      role="tab"
                      id={`hxsi-tab-${t.id}`}
                      aria-selected={selected}
                      aria-controls={`hxsi-panel-${t.id}`}
                      tabIndex={selected ? 0 : -1}
                      onClick={() => setActive(t.id)}
                      onKeyDown={onTabKey}
                      className={`rounded-md px-3.5 py-1.5 text-sm font-medium outline-none ring-indigo-500/50 transition focus-visible:ring-2 ${
                        selected
                          ? "bg-white text-slate-900 shadow-sm dark:bg-slate-950 dark:text-white"
                          : "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200"
                      }`}
                    >
                      {t.label}
                    </button>
                  );
                })}
              </div>
            </div>

            {/* panel */}
            <div
              role="tabpanel"
              id={`hxsi-panel-${active}`}
              aria-labelledby={`hxsi-tab-${active}`}
              className="p-5"
            >
              {active === "reports" ? (
                <ul className="space-y-2.5">
                  {REPORTS.map((r) => (
                    <li
                      key={r.name}
                      className="flex items-center justify-between gap-3 rounded-xl border border-slate-200 bg-slate-50/60 px-4 py-3 dark:border-slate-700/70 dark:bg-slate-800/40"
                    >
                      <div className="flex items-center gap-3">
                        <span className="flex h-9 w-9 items-center justify-center rounded-lg bg-indigo-500/10 text-indigo-600 dark:text-indigo-300">
                          <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden="true">
                            <path d="M6 2h9l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z" />
                            <path d="M14 2v6h6M8 13h8M8 17h5" />
                          </svg>
                        </span>
                        <div>
                          <p className="text-sm font-medium text-slate-800 dark:text-slate-100">{r.name}</p>
                          <p className="text-xs text-slate-500 dark:text-slate-400">{r.when}</p>
                        </div>
                      </div>
                      <StatusBadge status={r.status} />
                    </li>
                  ))}
                </ul>
              ) : (
                <>
                  {active === "overview" ? (
                    <div className="grid grid-cols-3 gap-3">
                      {KPIS.map((k) => (
                        <div
                          key={k.label}
                          className="rounded-xl border border-slate-200 bg-slate-50/60 p-3 dark:border-slate-700/70 dark:bg-slate-800/40"
                        >
                          <p className="truncate text-[11px] font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
                            {k.label}
                          </p>
                          <p className="mt-1 text-xl font-semibold text-slate-900 dark:text-white">{k.value}</p>
                          <p className="mt-0.5 text-xs font-medium text-emerald-600 dark:text-emerald-400">{k.delta}</p>
                        </div>
                      ))}
                    </div>
                  ) : (
                    <div className="space-y-2.5">
                      <div className="rounded-xl border border-slate-200 bg-slate-50/60 px-4 py-3 dark:border-slate-700/70 dark:bg-slate-800/40">
                        <p className="text-sm font-medium text-slate-800 dark:text-slate-100">Velocity up 18%</p>
                        <p className="text-xs text-slate-500 dark:text-slate-400">since teams moved to weekly planning cycles.</p>
                      </div>
                      <div className="rounded-xl border border-amber-500/25 bg-amber-500/5 px-4 py-3">
                        <p className="text-sm font-medium text-amber-700 dark:text-amber-300">2 teams flagged early</p>
                        <p className="text-xs text-amber-700/70 dark:text-amber-400/70">trending toward missed deadlines this sprint.</p>
                      </div>
                    </div>
                  )}

                  {/* chart */}
                  <div className="mt-4 rounded-xl border border-slate-200 bg-slate-50/60 p-4 dark:border-slate-700/70 dark:bg-slate-800/40">
                    <div className="mb-3 flex items-center justify-between">
                      <p className="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
                        Throughput · this week
                      </p>
                      <span className="inline-flex items-center gap-1 text-xs font-medium text-emerald-600 dark:text-emerald-400">
                        <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2.4" aria-hidden="true">
                          <path d="M4 14l5-5 4 4 7-7" />
                          <path d="M17 6h4v4" />
                        </svg>
                        +9.4%
                      </span>
                    </div>
                    <div className="flex h-32 items-end gap-2">
                      {bars.map((v, i) => (
                        <div key={i} className="flex flex-1 flex-col items-center gap-1.5">
                          <div className="flex w-full flex-1 items-end">
                            <div
                              className="w-full rounded-t-md bg-gradient-to-t from-indigo-500 to-violet-400 dark:from-indigo-500 dark:to-violet-300"
                              style={{
                                height: `${v}%`,
                                transitionProperty: "height",
                                transitionDuration: reduce ? "0ms" : "550ms",
                                transitionTimingFunction: "cubic-bezier(0.22,1,0.36,1)",
                              }}
                            />
                          </div>
                          <span className="text-[10px] font-medium text-slate-400 dark:text-slate-500">
                            {DAYS[i]}
                          </span>
                        </div>
                      ))}
                    </div>
                  </div>
                </>
              )}
            </div>
          </div>

          {/* floating badge */}
          <div
            className="hxsi-anim absolute -bottom-5 -left-5 hidden items-center gap-2.5 rounded-xl border border-slate-200 bg-white px-4 py-3 shadow-xl sm:flex dark:border-slate-700 dark:bg-slate-900"
            style={{ animation: reduce ? undefined : "hxsi_floatAlt 7s ease-in-out infinite" }}
          >
            <span className="flex h-9 w-9 items-center justify-center rounded-lg bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
              <svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
                <path d="M12 2v4M12 18v4M2 12h4M18 12h4" />
                <circle cx="12" cy="12" r="4" />
              </svg>
            </span>
            <div className="leading-tight">
              <p className="text-sm font-semibold text-slate-900 dark:text-white">Live sync</p>
              <p className="text-xs text-slate-500 dark:text-slate-400">updated 4s ago</p>
            </div>
          </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.