Card CTA
Original · freeboxed CTA card
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-card.json"use client";
import { useId, useState, type ChangeEvent, type FormEvent } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Audience = "teams" | "solo";
const AUDIENCE_COPY: Record<
Audience,
{ headline: string; sub: string; bullet: string }
> = {
teams: {
headline: "Give your team one place to see the whole picture.",
sub: "Beacon pulls tasks, docs, and status updates into a single live workspace — so no one has to ask “where are we on this?” across three different apps.",
bullet: "Unlimited members on every plan",
},
solo: {
headline: "Everything you're juggling, finally in one calm view.",
sub: "Beacon keeps your projects, notes, and deadlines in one workspace you'll actually want to open every morning — no setup marathon required.",
bullet: "Ready to use in under two minutes",
},
};
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const AVATARS: { initials: string; from: string; to: string }[] = [
{ initials: "AR", from: "from-rose-400", to: "to-amber-400" },
{ initials: "MK", from: "from-sky-400", to: "to-indigo-500" },
{ initials: "JL", from: "from-emerald-400", to: "to-teal-500" },
{ initials: "TN", from: "from-violet-400", to: "to-fuchsia-500" },
];
export default function CtaxCard() {
const reduce = useReducedMotion();
const [audience, setAudience] = useState<Audience>("teams");
const [email, setEmail] = useState("");
const [status, setStatus] = useState<"idle" | "error" | "success">("idle");
const emailId = useId();
const errorId = useId();
const copy = AUDIENCE_COPY[audience];
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setEmail(e.target.value);
if (status === "error") setStatus("idle");
}
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
if (!EMAIL_RE.test(email.trim())) {
setStatus("error");
return;
}
setStatus("success");
}
const swap = reduce ? 0 : 0.35;
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-20 sm:py-28 dark:bg-slate-950">
{/* ambient background shapes */}
<div
aria-hidden="true"
className="ctaxcard-blob pointer-events-none absolute -left-24 top-10 h-72 w-72 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-600/20"
/>
<div
aria-hidden="true"
className="ctaxcard-blob pointer-events-none absolute -right-24 bottom-0 h-80 w-80 rounded-full bg-violet-300/30 blur-3xl dark:bg-violet-700/20"
style={{ animationDelay: "1.4s" }}
/>
<motion.div
initial={reduce ? false : { opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: reduce ? 0 : 0.6, ease: "easeOut" }}
className="relative mx-auto max-w-4xl"
>
{/* rotating color halo */}
<div
aria-hidden="true"
className="ctaxcard-glow absolute -inset-6 rounded-[3rem] opacity-25 blur-2xl dark:opacity-40"
style={{
background:
"conic-gradient(from 0deg, #6366f1, #a855f7, #38bdf8, #6366f1)",
}}
/>
<div className="relative overflow-hidden rounded-[2rem] bg-gradient-to-b from-white to-slate-50 p-8 shadow-2xl ring-1 ring-slate-200 sm:p-12 dark:from-slate-900 dark:to-slate-950 dark:ring-slate-800">
{/* dot grid texture */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 opacity-[0.35] dark:opacity-20"
style={{
backgroundImage:
"radial-gradient(currentColor 1px, transparent 1px)",
backgroundSize: "22px 22px",
color: "#94a3b8",
maskImage:
"linear-gradient(to bottom, black, transparent 65%)",
WebkitMaskImage:
"linear-gradient(to bottom, black, transparent 65%)",
}}
/>
<div className="relative">
{/* top row: badge + audience switch */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<span className="inline-flex w-fit items-center gap-2 rounded-full bg-indigo-50 px-3 py-1 text-xs font-semibold tracking-wide text-indigo-700 ring-1 ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/20">
<span className="relative flex h-2 w-2">
<span className="ctaxcard-dot absolute inline-flex h-full w-full rounded-full bg-indigo-500 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-indigo-500" />
</span>
NOW IN OPEN BETA
</span>
<div
role="group"
aria-label="Choose what to see"
className="inline-flex self-start rounded-full bg-slate-100 p-1 text-sm font-medium ring-1 ring-slate-200 sm:self-auto dark:bg-slate-800/80 dark:ring-slate-700"
>
{(["teams", "solo"] as const).map((key) => {
const active = audience === key;
return (
<button
key={key}
type="button"
aria-pressed={active}
onClick={() => setAudience(key)}
className={
"rounded-full px-4 py-1.5 transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 motion-reduce:transition-none dark:focus-visible:ring-offset-slate-800 " +
(active
? "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")
}
>
{key === "teams" ? "For teams" : "For solo work"}
</button>
);
})}
</div>
</div>
{/* swappable copy block */}
<AnimatePresence mode="wait">
<motion.div
key={audience}
initial={reduce ? false : { opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 1 } : { opacity: 0, y: -10 }}
transition={{ duration: swap, ease: "easeOut" }}
className="mt-7 max-w-2xl"
>
<h2 className="text-3xl font-bold leading-tight tracking-tight text-slate-900 sm:text-4xl dark:text-white">
{copy.headline}
</h2>
<p className="mt-4 text-base leading-relaxed text-slate-600 sm:text-lg dark:text-slate-300">
{copy.sub}
</p>
<p className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-emerald-700 dark:text-emerald-400">
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
className="h-5 w-5 shrink-0"
>
<circle cx="10" cy="10" r="9" className="fill-emerald-100 dark:fill-emerald-500/15" />
<path
d="M6 10.5l2.5 2.5L14 7.5"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
{copy.bullet}
</p>
</motion.div>
</AnimatePresence>
{/* form / success swap */}
<div className="mt-8">
<AnimatePresence mode="wait" initial={false}>
{status === "success" ? (
<motion.div
key="success"
initial={reduce ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 1 } : { opacity: 0, y: -8 }}
transition={{ duration: swap, ease: "easeOut" }}
role="status"
aria-live="polite"
className="flex items-start gap-3 rounded-2xl bg-emerald-50 p-5 ring-1 ring-emerald-200 dark:bg-emerald-500/10 dark:ring-emerald-400/20"
>
<svg
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
className="mt-0.5 h-6 w-6 shrink-0 text-emerald-600 dark:text-emerald-400"
>
<path
d="M20 6L9 17l-5-5"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<div>
<p className="font-semibold text-emerald-900 dark:text-emerald-200">
You're on the list.
</p>
<p className="mt-1 text-sm text-emerald-800/80 dark:text-emerald-300/80">
We sent a confirmation to{" "}
<span className="font-medium">{email.trim()}</span>. Tap
the link and your workspace will be ready.
</p>
</div>
</motion.div>
) : (
<motion.form
key="form"
initial={reduce ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 1 } : { opacity: 0, y: -8 }}
transition={{ duration: swap, ease: "easeOut" }}
onSubmit={handleSubmit}
noValidate
className="flex flex-col gap-3"
>
<div className="flex flex-col gap-3 sm:flex-row">
<div className="flex-1">
<label htmlFor={emailId} className="sr-only">
Work email
</label>
<input
id={emailId}
name="email"
type="email"
inputMode="email"
autoComplete="email"
placeholder="you@company.com"
value={email}
onChange={handleChange}
aria-invalid={status === "error"}
aria-describedby={
status === "error" ? errorId : undefined
}
className={
"w-full rounded-xl border bg-white px-4 py-3 text-base text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-slate-900 dark:text-white dark:placeholder:text-slate-500 " +
(status === "error"
? "border-rose-400 focus:ring-rose-500 dark:border-rose-500"
: "border-slate-300 dark:border-slate-700")
}
/>
</div>
<button
type="submit"
className="group relative inline-flex items-center justify-center gap-2 overflow-hidden rounded-xl bg-gradient-to-br from-indigo-600 to-violet-600 px-6 py-3 text-base font-semibold text-white shadow-lg shadow-indigo-600/20 transition-transform duration-200 hover:-translate-y-0.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white motion-reduce:transition-none motion-reduce:hover:translate-y-0 dark:focus-visible:ring-offset-slate-900"
>
<span
aria-hidden="true"
className="absolute inset-0 -translate-x-full -skew-x-12 bg-white/25 transition-transform duration-700 group-hover:translate-x-full motion-reduce:hidden"
/>
<span className="relative">Start free</span>
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
className="relative h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5 motion-reduce:transition-none"
>
<path
d="M4 10h11M11 5l5 5-5 5"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>
{status === "error" ? (
<p
id={errorId}
role="alert"
className="text-sm font-medium text-rose-600 dark:text-rose-400"
>
Please enter a valid email address so we can send your
invite.
</p>
) : (
<p className="text-sm text-slate-500 dark:text-slate-400">
Free 14-day trial · No credit card · Cancel anytime.
</p>
)}
</motion.form>
)}
</AnimatePresence>
</div>
{/* divider */}
<div className="mt-8 h-px w-full bg-slate-200 dark:bg-slate-800" />
{/* trust row */}
<div className="mt-6 flex flex-col gap-5 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<div className="flex -space-x-2">
{AVATARS.map((a) => (
<span
key={a.initials}
className={
"flex h-9 w-9 items-center justify-center rounded-full bg-gradient-to-br text-xs font-semibold text-white ring-2 ring-white dark:ring-slate-900 " +
a.from +
" " +
a.to
}
>
{a.initials}
</span>
))}
</div>
<p className="text-sm text-slate-600 dark:text-slate-400">
Trusted by{" "}
<span className="font-semibold text-slate-900 dark:text-white">
12,000+
</span>{" "}
teams
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex" aria-hidden="true">
{[0, 1, 2, 3, 4].map((i) => (
<svg
key={i}
viewBox="0 0 20 20"
className="h-4 w-4 fill-amber-400"
>
<path d="M10 1.5l2.6 5.3 5.8.8-4.2 4.1 1 5.8L10 14.9 4.8 17.5l1-5.8L1.6 7.6l5.8-.8z" />
</svg>
))}
</div>
<p className="text-sm text-slate-600 dark:text-slate-400">
<span className="font-semibold text-slate-900 dark:text-white">
4.9
</span>{" "}
from 900+ reviews
</p>
</div>
</div>
</div>
</div>
</motion.div>
<style>{`
@keyframes ctaxcard-spin {
to { transform: rotate(360deg); }
}
@keyframes ctaxcard-dotpulse {
0%, 100% { opacity: .75; transform: scale(1); }
50% { opacity: 0; transform: scale(2.2); }
}
@keyframes ctaxcard-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-18px); }
}
.ctaxcard-glow { animation: ctaxcard-spin 9s linear infinite; }
.ctaxcard-dot { animation: ctaxcard-dotpulse 2s ease-in-out infinite; }
.ctaxcard-blob { animation: ctaxcard-float 7s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.ctaxcard-glow,
.ctaxcard-dot,
.ctaxcard-blob {
animation: none !important;
}
}
`}</style>
</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

Split CTA
Originalsplit CTA with image side

Gradient Band CTA
Originalgradient CTA band

Newsletter CTA
OriginalCTA with newsletter input

App Download CTA
Originalapp download CTA with store buttons

