Split Image Hero
Original · freesplit hero with copy and image
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"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
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 quoteSimilar components
Browse all →
Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

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

App Preview Hero
OriginalA 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
OriginalA dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

Image Backdrop Hero
OriginalA 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
OriginalA 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
OriginalA 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
OriginalA 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
OriginalA 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
MITCentered, 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
MITTwo-column hero on a dark background with a headline, paragraph, and two buttons beside a gradient media panel. Responsive column stack on mobile.

