Split CTA
Original · freesplit CTA with image side
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/ctax-split.json"use client";
import { useState, type ChangeEvent, type FormEvent } from "react";
import { motion, useReducedMotion, type Variants } from "motion/react";
/* ---------------------------------------------------------------- icons */
function IconArrow({ className }: { className?: string }) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={className}>
<path
d="M4 10h11m0 0-4.5-4.5M15 10l-4.5 4.5"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function IconCheck({ className }: { className?: string }) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={className}>
<path
d="m4.5 10.5 3.5 3.5 7.5-8"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function IconStar({ className }: { className?: string }) {
return (
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" className={className}>
<path d="M10 1.6l2.47 5.01 5.53.8-4 3.9.94 5.5L10 14.2l-4.94 2.6.94-5.5-4-3.9 5.53-.8L10 1.6z" />
</svg>
);
}
function IconBolt({ className }: { className?: string }) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={className}>
<path
d="M11 2 4 11h5l-1 7 7-9h-5l1-7z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</svg>
);
}
function IconSpark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={className}>
<path
d="M10 2.5v4M10 13.5v4M2.5 10h4M13.5 10h4M5 5l2.5 2.5M12.5 12.5 15 15M15 5l-2.5 2.5M7.5 12.5 5 15"
stroke="currentColor"
strokeWidth="1.4"
strokeLinecap="round"
/>
</svg>
);
}
/* ------------------------------------------------------------- animation */
const container: Variants = {
hidden: {},
show: { transition: { staggerChildren: 0.08, delayChildren: 0.04 } },
};
const rise: Variants = {
hidden: { opacity: 0, y: 18 },
show: {
opacity: 1,
y: 0,
transition: { duration: 0.55, ease: [0.22, 1, 0.36, 1] },
},
};
const still: Variants = {
hidden: { opacity: 1, y: 0 },
show: { opacity: 1, y: 0 },
};
/* ----------------------------------------------------------------- data */
const bars: number[] = [38, 61, 49, 82, 70, 94, 58];
const perks: string[] = ["No credit card", "14-day full access", "Cancel anytime"];
/* ------------------------------------------------------------ component */
export default function CtaxSplit() {
const reduce = useReducedMotion();
const variant = reduce ? still : rise;
const [email, setEmail] = useState<string>("");
const [status, setStatus] = useState<"idle" | "invalid" | "success">("idle");
const isValid = (value: string): boolean =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setEmail(e.target.value);
if (status === "invalid") setStatus("idle");
}
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
if (!isValid(email)) {
setStatus("invalid");
return;
}
setStatus("success");
}
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-20 sm:py-28 dark:bg-slate-950">
<style>{`
@keyframes ctaxsplit-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
@keyframes ctaxsplit-floatslow {
0%, 100% { transform: translateY(0) translateX(0); }
50% { transform: translateY(10px) translateX(-6px); }
}
@keyframes ctaxsplit-drift {
0%, 100% { transform: translate(0, 0) scale(1); opacity: .8; }
50% { transform: translate(18px, -14px) scale(1.08); opacity: 1; }
}
@keyframes ctaxsplit-sheen {
0% { transform: translateX(-120%); }
60%, 100% { transform: translateX(220%); }
}
@keyframes ctaxsplit-rise {
0% { transform: scaleY(.55); opacity: .55; }
100% { transform: scaleY(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.ctaxsplit-anim { animation: none !important; }
}
`}</style>
{/* ambient background wash */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<div className="ctaxsplit-anim absolute -top-24 left-1/4 h-72 w-72 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-600/20 [animation:ctaxsplit-drift_14s_ease-in-out_infinite]" />
<div className="ctaxsplit-anim absolute -bottom-28 right-1/5 h-80 w-80 rounded-full bg-violet-300/30 blur-3xl dark:bg-violet-700/20 [animation:ctaxsplit-drift_18s_ease-in-out_infinite_reverse]" />
</div>
<motion.div
variants={container}
initial="hidden"
whileInView="show"
viewport={{ once: true, amount: 0.3 }}
className="relative mx-auto grid max-w-6xl overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-xl shadow-slate-900/5 lg:grid-cols-2 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/30"
>
{/* ------------------------------------------------ copy / form side */}
<div className="flex flex-col justify-center gap-7 p-8 sm:p-12 lg:p-14">
<motion.span
variants={variant}
className="inline-flex w-fit items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3.5 py-1.5 text-xs font-semibold tracking-wide text-indigo-700 uppercase dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300"
>
<IconSpark className="h-3.5 w-3.5" />
Now in early access
</motion.span>
<motion.h2
variants={variant}
className="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-[2.6rem] lg:leading-[1.1] dark:text-white"
>
Ship your next release{" "}
<span className="bg-gradient-to-r from-indigo-600 via-violet-600 to-indigo-500 bg-clip-text text-transparent dark:from-indigo-400 dark:via-violet-400 dark:to-indigo-300">
without the busywork
</span>
</motion.h2>
<motion.p
variants={variant}
className="max-w-md text-base leading-relaxed text-slate-600 sm:text-lg dark:text-slate-300"
>
Northline turns scattered specs, tickets, and reviews into one
planning workspace — so your team spends less time coordinating and
more time shipping.
</motion.p>
{/* email capture */}
<motion.div variants={variant} className="w-full max-w-md">
{status === "success" ? (
<div
role="status"
aria-live="polite"
className="flex items-start gap-3 rounded-2xl border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-500/30 dark:bg-emerald-500/10"
>
<span className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white">
<IconCheck className="h-4 w-4" />
</span>
<div className="text-sm">
<p className="font-semibold text-emerald-800 dark:text-emerald-200">
You're on the list.
</p>
<p className="mt-0.5 text-emerald-700/90 dark:text-emerald-300/80">
We sent a setup link to{" "}
<span className="font-medium">{email.trim()}</span>.
</p>
<button
type="button"
onClick={() => {
setEmail("");
setStatus("idle");
}}
className="mt-2 rounded-sm text-xs font-semibold text-emerald-700 underline underline-offset-2 hover:text-emerald-900 focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:outline-none dark:text-emerald-300 dark:hover:text-emerald-100"
>
Use a different email
</button>
</div>
</div>
) : (
<form onSubmit={handleSubmit} noValidate className="flex flex-col gap-2">
<div className="flex flex-col gap-2 sm:flex-row">
<div className="flex-1">
<label htmlFor="ctaxsplit-email" className="sr-only">
Work email
</label>
<input
id="ctaxsplit-email"
type="email"
inputMode="email"
autoComplete="email"
value={email}
onChange={handleChange}
placeholder="you@company.com"
aria-invalid={status === "invalid"}
aria-describedby={
status === "invalid" ? "ctaxsplit-error" : undefined
}
className={`w-full rounded-xl border bg-white px-4 py-3 text-sm text-slate-900 placeholder-slate-400 transition focus-visible:ring-2 focus-visible:outline-none dark:bg-slate-800 dark:text-white dark:placeholder-slate-500 ${
status === "invalid"
? "border-rose-400 focus-visible:ring-rose-500 dark:border-rose-500/60"
: "border-slate-300 focus-visible:ring-indigo-500 dark:border-slate-700"
}`}
/>
</div>
<button
type="submit"
className="group inline-flex items-center justify-center gap-2 rounded-xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-sm shadow-indigo-600/20 transition hover:bg-indigo-500 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:outline-none active:translate-y-px dark:focus-visible:ring-offset-slate-900"
>
Start free
<IconArrow className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
</button>
</div>
{status === "invalid" && (
<p
id="ctaxsplit-error"
role="alert"
className="text-xs font-medium text-rose-600 dark:text-rose-400"
>
Please enter a valid email address.
</p>
)}
</form>
)}
<ul className="mt-4 flex flex-wrap gap-x-5 gap-y-2">
{perks.map((perk) => (
<li
key={perk}
className="flex items-center gap-1.5 text-xs font-medium text-slate-500 dark:text-slate-400"
>
<IconCheck className="h-3.5 w-3.5 text-emerald-500" />
{perk}
</li>
))}
</ul>
</motion.div>
{/* social proof */}
<motion.div
variants={variant}
className="flex items-center gap-3 border-t border-slate-200 pt-6 dark:border-slate-800"
>
<div className="flex -space-x-2" aria-hidden="true">
{["from-indigo-400 to-violet-500", "from-sky-400 to-indigo-500", "from-violet-400 to-rose-400", "from-emerald-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-900`}
/>
),
)}
</div>
<div className="text-xs leading-tight text-slate-600 dark:text-slate-400">
<span className="flex items-center gap-1 text-amber-500">
{Array.from({ length: 5 }).map((_, i) => (
<IconStar key={i} className="h-3.5 w-3.5" />
))}
<span className="ml-1 font-semibold text-slate-800 dark:text-slate-200">
4.9/5
</span>
</span>
<span>Loved by 4,000+ product teams</span>
</div>
</motion.div>
</div>
{/* --------------------------------------------------- visual side */}
<div className="relative min-h-[22rem] overflow-hidden bg-gradient-to-br from-indigo-600 via-violet-600 to-indigo-700 p-8 sm:p-12 lg:p-14 dark:from-indigo-700 dark:via-violet-800 dark:to-slate-900">
{/* grid + glow decor */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<div
className="absolute inset-0 opacity-[0.18]"
style={{
backgroundImage:
"linear-gradient(rgba(255,255,255,.6) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.6) 1px, transparent 1px)",
backgroundSize: "34px 34px",
maskImage:
"radial-gradient(120% 90% at 70% 15%, #000 40%, transparent 80%)",
WebkitMaskImage:
"radial-gradient(120% 90% at 70% 15%, #000 40%, transparent 80%)",
}}
/>
<div className="ctaxsplit-anim absolute -right-10 -top-10 h-56 w-56 rounded-full bg-white/20 blur-3xl [animation:ctaxsplit-drift_16s_ease-in-out_infinite]" />
</div>
{/* dashboard glass card */}
<div className="relative flex h-full items-center justify-center">
<div className="ctaxsplit-anim relative w-full max-w-sm rounded-2xl border border-white/25 bg-white/12 p-5 shadow-2xl shadow-black/20 backdrop-blur-md [animation:ctaxsplit-float_7s_ease-in-out_infinite]">
{/* sheen sweep */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 overflow-hidden rounded-2xl"
>
<div className="ctaxsplit-anim absolute inset-y-0 -left-1/3 w-1/3 skew-x-12 bg-gradient-to-r from-transparent via-white/25 to-transparent [animation:ctaxsplit-sheen_6s_ease-in-out_infinite]" />
</div>
<div className="relative">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-white/25 text-white">
<IconBolt className="h-4 w-4" />
</span>
<div className="leading-tight">
<p className="text-sm font-semibold text-white">Sprint 24</p>
<p className="text-[11px] text-white/70">On track</p>
</div>
</div>
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-400/90 px-2 py-0.5 text-[11px] font-semibold text-emerald-950">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-950/70" />
Live
</span>
</div>
<div className="mt-5">
<p className="text-xs text-white/70">Cycle time this month</p>
<p className="mt-1 flex items-end gap-2">
<span className="text-3xl font-bold tracking-tight text-white">
2.4d
</span>
<span className="mb-1 text-xs font-semibold text-emerald-300">
-38%
</span>
</p>
</div>
{/* bar chart */}
<div
className="mt-4 flex h-24 items-end gap-2"
aria-hidden="true"
>
{bars.map((h, i) => (
<div
key={i}
className="ctaxsplit-anim flex-1 origin-bottom rounded-t-md bg-gradient-to-t from-white/40 to-white/85"
style={{
height: `${h}%`,
animation: "ctaxsplit-rise .8s ease-out both",
animationDelay: `${i * 90}ms`,
}}
/>
))}
</div>
<div className="mt-5 space-y-2.5">
{[
{ label: "Auth service", value: "Merged" },
{ label: "Billing refactor", value: "In review" },
].map((row) => (
<div
key={row.label}
className="flex items-center justify-between rounded-lg bg-white/10 px-3 py-2"
>
<span className="flex items-center gap-2 text-xs font-medium text-white">
<span className="h-1.5 w-1.5 rounded-full bg-violet-200" />
{row.label}
</span>
<span className="text-[11px] text-white/70">
{row.value}
</span>
</div>
))}
</div>
</div>
</div>
{/* floating badge */}
<div className="ctaxsplit-anim absolute bottom-6 left-4 hidden items-center gap-2 rounded-xl border border-white/25 bg-white/15 px-3 py-2 shadow-lg backdrop-blur-md sm:flex [animation:ctaxsplit-floatslow_9s_ease-in-out_infinite]">
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-400 text-emerald-950">
<IconCheck className="h-4 w-4" />
</span>
<div className="leading-tight">
<p className="text-xs font-semibold text-white">Deploy ready</p>
<p className="text-[10px] text-white/70">All checks passed</p>
</div>
</div>
</div>
</div>
</motion.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 →
Gradient CTA
OriginalA bold gradient call-to-action banner with two buttons.

Full-Bleed Dark CTA
OriginalA full-bleed dark call-to-action banner with a live status badge, subtle grid backdrop and a primary plus secondary button pair to drive discovery calls.

Split CTA With Lead Form
OriginalA two-column call-to-action that pairs a benefit checklist with an inline name, email and message form for capturing qualified leads directly in the section.

Newsletter CTA With Stats
OriginalA newsletter sign-up call-to-action combining an inline email subscribe form with a three-cell social-proof stat strip to boost conversions.

Aurora Glow CTA Banner
OriginalA full-bleed dark call-to-action banner with a rotating aurora gradient, floating orbs and a shine-on-hover button, with staggered blur-in text that reveals on scroll.

Animated Border Beam CTA
OriginalAn email-capture call-to-action card wrapped in a rotating conic-gradient border beam, with staggered reveal, animated shine text and an inline success state on submit.

Marquee Ribbon CTA
OriginalA dark call-to-action band framed by two infinite scrolling tag ribbons with a continuously flowing animated-gradient button and staggered centre content.

Centered CTA
Originalcentred CTA band

Gradient Band CTA
Originalgradient CTA band

Newsletter CTA
OriginalCTA with newsletter input

App Download CTA
Originalapp download CTA with store buttons

Stats CTA CTA
OriginalCTA with supporting stats

