Newsletter CTA
Original · freeCTA with newsletter input
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-newsletter.json"use client";
import { useId, useState } from "react";
import type { ChangeEvent, FormEvent } from "react";
import { motion, useReducedMotion } from "motion/react";
type Status = "idle" | "error" | "success";
const EASE = [0.22, 1, 0.36, 1] as const;
const AVATARS = [
"from-indigo-400 to-violet-500",
"from-sky-400 to-indigo-500",
"from-rose-400 to-amber-400",
"from-emerald-400 to-sky-500",
"from-violet-400 to-rose-400",
];
const PERKS = [
{
label: "Every Tuesday",
d: "M8 2v3m8-3v3M3 9h18M5 5h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Z",
},
{
label: "A 5-minute read",
d: "M12 7v5l3 2m6-2a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z",
},
{
label: "One-click unsubscribe",
d: "M5 13l4 4L19 7",
},
];
export default function CtaxNewsletter() {
const reduce = useReducedMotion();
const emailId = useId();
const noteId = useId();
const errorId = useId();
const [email, setEmail] = useState("");
const [status, setStatus] = useState<Status>("idle");
const isValidEmail = (value: string): boolean =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
event.preventDefault();
if (!isValidEmail(email)) {
setStatus("error");
return;
}
setStatus("success");
};
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
setEmail(event.target.value);
if (status === "error") setStatus("idle");
};
const reset = (): void => {
setEmail("");
setStatus("idle");
};
const container = {
hidden: {},
show: {
transition: { staggerChildren: reduce ? 0 : 0.08, delayChildren: 0.04 },
},
};
const item = {
hidden: { opacity: 0, y: reduce ? 0 : 18 },
show: {
opacity: 1,
y: 0,
transition: { duration: reduce ? 0 : 0.6, ease: EASE },
},
};
return (
<section className="relative w-full overflow-hidden bg-white px-4 py-20 text-neutral-900 sm:py-28 lg:py-32 dark:bg-neutral-950 dark:text-neutral-100">
<style>{`
@keyframes ctaxnl-float {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(24px, -30px, 0) scale(1.08); }
}
@keyframes ctaxnl-drift {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(-28px, 22px, 0) scale(1.12); }
}
@keyframes ctaxnl-sheen {
0% { transform: translateX(-140%) skewX(-18deg); }
60%, 100% { transform: translateX(320%) skewX(-18deg); }
}
@keyframes ctaxnl-ping {
0% { transform: scale(1); opacity: 0.7; }
70%, 100% { transform: scale(2.4); opacity: 0; }
}
@keyframes ctaxnl-pop {
0% { transform: scale(0.6); opacity: 0; }
60% { transform: scale(1.12); }
100% { transform: scale(1); opacity: 1; }
}
.ctaxnl-float { animation: ctaxnl-float 14s ease-in-out infinite; }
.ctaxnl-drift { animation: ctaxnl-drift 18s ease-in-out infinite; }
.ctaxnl-sheen { animation: ctaxnl-sheen 4.6s ease-in-out infinite; }
.ctaxnl-ping { animation: ctaxnl-ping 2.4s cubic-bezier(0,0,0.2,1) infinite; }
.ctaxnl-pop { animation: ctaxnl-pop 0.5s cubic-bezier(0.22,1,0.36,1) both; }
@media (prefers-reduced-motion: reduce) {
.ctaxnl-float, .ctaxnl-drift, .ctaxnl-sheen, .ctaxnl-ping, .ctaxnl-pop {
animation: none !important;
}
}
`}</style>
{/* Decorative background */}
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10">
<div className="ctaxnl-float absolute -left-24 -top-24 h-80 w-80 rounded-full bg-indigo-300/40 blur-3xl dark:bg-indigo-600/25" />
<div className="ctaxnl-drift absolute -bottom-32 -right-16 h-96 w-96 rounded-full bg-violet-300/40 blur-3xl dark:bg-violet-700/25" />
<div className="ctaxnl-float absolute right-1/3 top-1/4 h-56 w-56 rounded-full bg-sky-200/40 blur-3xl dark:bg-sky-700/20" />
<svg
className="absolute inset-0 h-full w-full text-neutral-900/[0.035] dark:text-white/[0.04]"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<pattern
id="ctaxnl-grid"
width="32"
height="32"
patternUnits="userSpaceOnUse"
>
<path d="M32 0H0V32" fill="none" stroke="currentColor" strokeWidth="1" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#ctaxnl-grid)" />
</svg>
</div>
<motion.div
variants={container}
initial="hidden"
whileInView="show"
viewport={{ once: true, amount: 0.3 }}
className="relative mx-auto max-w-3xl"
>
<div className="relative overflow-hidden rounded-3xl border border-neutral-200/80 bg-neutral-50/80 p-8 shadow-xl shadow-neutral-900/5 backdrop-blur-sm sm:p-12 dark:border-white/10 dark:bg-neutral-900/70 dark:shadow-black/40">
{/* top accent line */}
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-indigo-400/70 to-transparent dark:via-violet-400/60" />
{/* Eyebrow */}
<motion.div variants={item} className="flex justify-center">
<span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3.5 py-1.5 text-xs font-semibold uppercase tracking-wider text-indigo-700 dark:border-indigo-400/20 dark:bg-indigo-400/10 dark:text-indigo-300">
<span className="relative flex h-2 w-2">
<span className="ctaxnl-ping absolute inline-flex h-full w-full rounded-full bg-indigo-500 dark:bg-indigo-400" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-indigo-500 dark:bg-indigo-400" />
</span>
The Cadence Brief
</span>
</motion.div>
<motion.h2
variants={item}
className="mt-6 text-center text-3xl font-bold tracking-tight text-balance sm:text-4xl lg:text-[2.75rem] lg:leading-[1.1]"
>
Ship better products,{" "}
<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">
one Tuesday at a time
</span>
</motion.h2>
<motion.p
variants={item}
className="mx-auto mt-4 max-w-xl text-center text-base leading-relaxed text-neutral-600 text-pretty sm:text-lg dark:text-neutral-400"
>
Join 34,000+ founders, designers, and engineers reading a five-minute
field note on the craft of building things people actually want.
Practical, opinionated, and free.
</motion.p>
{/* Form / Success */}
<motion.div variants={item} className="mx-auto mt-8 max-w-lg">
{status === "success" ? (
<div
role="status"
aria-live="polite"
className="ctaxnl-pop flex flex-col items-center rounded-2xl border border-emerald-200 bg-emerald-50 px-6 py-8 text-center dark:border-emerald-400/20 dark:bg-emerald-400/10"
>
<span className="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-500 text-white shadow-lg shadow-emerald-500/30">
<svg
viewBox="0 0 24 24"
className="h-6 w-6"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M5 13l4 4L19 7" />
</svg>
</span>
<p className="mt-4 text-lg 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 just sent a confirmation to{" "}
<span className="font-medium">{email.trim()}</span>. Click the
link inside and your first brief lands this Tuesday.
</p>
<button
type="button"
onClick={reset}
className="mt-5 inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-semibold text-emerald-700 underline-offset-4 hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 dark:text-emerald-300"
>
Use a different email
</button>
</div>
) : (
<form onSubmit={handleSubmit} noValidate className="w-full">
<label htmlFor={emailId} className="sr-only">
Email address
</label>
<div className="flex flex-col gap-3 sm:flex-row">
<div className="relative flex-1">
<span
aria-hidden
className="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 dark:text-neutral-500"
>
<svg
viewBox="0 0 24 24"
className="h-5 w-5"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="5" width="18" height="14" rx="2" />
<path d="m3 7 9 6 9-6" />
</svg>
</span>
<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 : noteId
}
className={`w-full rounded-xl border bg-white py-3.5 pl-11 pr-4 text-base text-neutral-900 shadow-sm transition-colors placeholder:text-neutral-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50 dark:bg-neutral-950/60 dark:text-neutral-100 dark:placeholder:text-neutral-500 dark:focus-visible:ring-offset-neutral-900 ${
status === "error"
? "border-rose-400 focus-visible:ring-rose-500 dark:border-rose-500/60"
: "border-neutral-300 focus-visible:ring-indigo-500 dark:border-white/15"
}`}
/>
</div>
<button
type="submit"
className="group relative inline-flex shrink-0 items-center justify-center gap-2 overflow-hidden rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 px-6 py-3.5 text-base font-semibold text-white shadow-lg shadow-indigo-600/25 transition-[transform,box-shadow] hover:shadow-xl hover:shadow-indigo-600/30 active:translate-y-px focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50 dark:focus-visible:ring-offset-neutral-900"
>
<span
aria-hidden
className="ctaxnl-sheen absolute inset-y-0 left-0 w-1/3 bg-white/25 blur-sm"
/>
<span className="relative">Get the brief</span>
<svg
viewBox="0 0 24 24"
className="relative h-4.5 w-4.5 transition-transform group-hover:translate-x-0.5"
fill="none"
stroke="currentColor"
strokeWidth="2.25"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</button>
</div>
{status === "error" ? (
<p
id={errorId}
role="alert"
className="mt-2.5 flex items-center gap-1.5 pl-1 text-sm font-medium text-rose-600 dark:text-rose-400"
>
<svg
viewBox="0 0 24 24"
className="h-4 w-4 shrink-0"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<circle cx="12" cy="12" r="9" />
<path d="M12 8v4m0 4h.01" />
</svg>
Please enter a valid email address.
</p>
) : (
<p
id={noteId}
className="mt-2.5 pl-1 text-sm text-neutral-500 dark:text-neutral-500"
>
No spam, ever. Unsubscribe with a single click.
</p>
)}
</form>
)}
</motion.div>
{/* Perks */}
<motion.ul
variants={item}
className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-3"
>
{PERKS.map((perk) => (
<li
key={perk.label}
className="flex items-center gap-1.5 text-sm font-medium text-neutral-600 dark:text-neutral-400"
>
<svg
viewBox="0 0 24 24"
className="h-4 w-4 text-indigo-500 dark:text-indigo-400"
fill="none"
stroke="currentColor"
strokeWidth="1.9"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d={perk.d} />
</svg>
{perk.label}
</li>
))}
</motion.ul>
{/* Social proof */}
<motion.div
variants={item}
className="mt-8 flex flex-col items-center justify-center gap-3 border-t border-neutral-200/70 pt-6 sm:flex-row dark:border-white/10"
>
<div className="flex -space-x-2.5">
{AVATARS.map((gradient, index) => (
<span
key={gradient}
className={`inline-flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br ring-2 ring-neutral-50 dark:ring-neutral-900 ${gradient}`}
aria-hidden
>
<span className="text-[0.65rem] font-bold text-white/90">
{String.fromCharCode(65 + index)}
</span>
</span>
))}
</div>
<p className="text-center text-sm text-neutral-600 sm:text-left dark:text-neutral-400">
<span className="font-semibold text-neutral-900 dark:text-neutral-100">
34,000+ builders
</span>{" "}
read it every week
<span className="ml-2 inline-flex translate-y-0.5 gap-0.5 align-middle text-amber-500">
{Array.from({ length: 5 }).map((_, index) => (
<svg
key={index}
viewBox="0 0 24 24"
className="h-3.5 w-3.5"
fill="currentColor"
aria-hidden
>
<path d="M12 2.5l2.9 5.9 6.5.95-4.7 4.58 1.11 6.47L12 17.9l-5.81 3.06 1.11-6.47-4.7-4.58 6.5-.95L12 2.5Z" />
</svg>
))}
</span>
</p>
</motion.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

Split CTA
Originalsplit CTA with image side

Gradient Band CTA
Originalgradient CTA band

App Download CTA
Originalapp download CTA with store buttons

Stats CTA CTA
OriginalCTA with supporting stats

