Benefits Newsletter
Original · freenewsletter with benefit bullets
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/newsx-benefits.json"use client";
import {
useState,
useRef,
useEffect,
useId,
type FormEvent,
type ChangeEvent,
type ReactNode,
} from "react";
import {
motion,
AnimatePresence,
useReducedMotion,
type Variants,
} from "motion/react";
type Status = "idle" | "loading" | "success";
type Cadence = "weekly" | "monthly";
interface Benefit {
title: string;
body: string;
icon: ReactNode;
}
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
const BENEFITS: Benefit[] = [
{
title: "One deep teardown every Sunday",
body: "A real shipping interface pulled apart to the pixel — type ramp, spacing logic, and the motion curves behind it.",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5">
<path d="M10.5 3.5h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1Z" />
<path d="M20.5 12.5h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1Z" />
<path d="M13.5 3.5h6a1 1 0 0 1 1 1v3.5" />
<path d="M10.5 20.5h-6a1 1 0 0 1-1-1V16" />
</svg>
),
},
{
title: "Copy-paste recipes, not theory",
body: "The exact CSS, easing curves, and layout math — dropped in a block you can lift straight into your next build.",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5">
<path d="m8 7-4.5 5L8 17" />
<path d="m16 7 4.5 5L16 17" />
<path d="m13.5 4-3 16" />
</svg>
),
},
{
title: "No sponsors, no tracking pixels",
body: "The letter is funded by readers, so nothing is sold to you and nothing is watching you read. Ever.",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5">
<path d="M12 3 4.5 6v5.5c0 4.4 3.1 7.6 7.5 9 4.4-1.4 7.5-4.6 7.5-9V6L12 3Z" />
<path d="m9 12 2 2 4-4.5" />
</svg>
),
},
{
title: "141 issues, fully searchable",
body: "The whole archive is open and free — years of teardowns indexed by pattern, component, and technique.",
icon: (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5">
<path d="M3.5 7.5 12 4l8.5 3.5L12 11 3.5 7.5Z" />
<path d="m3.5 12 8.5 3.5L20.5 12" />
<path d="m3.5 16.5 8.5 3.5 8.5-3.5" />
</svg>
),
},
];
const listContainer: Variants = {
hidden: {},
show: { transition: { staggerChildren: 0.09, delayChildren: 0.12 } },
};
const listItem: Variants = {
hidden: { opacity: 0, y: 18 },
show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } },
};
export default function NewsxBenefits() {
const reduce = useReducedMotion();
const uid = useId();
const [email, setEmail] = useState("");
const [cadence, setCadence] = useState<Cadence>("weekly");
const [status, setStatus] = useState<Status>("idle");
const [error, setError] = useState<string | null>(null);
const timer = useRef<number | null>(null);
useEffect(() => {
return () => {
if (timer.current !== null) window.clearTimeout(timer.current);
};
}, []);
function onEmailChange(e: ChangeEvent<HTMLInputElement>) {
setEmail(e.target.value);
if (error) setError(null);
}
function onSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
if (status === "loading") return;
const value = email.trim();
if (!EMAIL_RE.test(value)) {
setError("Enter a valid email so we know where to send it.");
return;
}
setError(null);
setStatus("loading");
timer.current = window.setTimeout(() => setStatus("success"), 1150);
}
function reset() {
setStatus("idle");
setEmail("");
setError(null);
}
const emailId = `${uid}-email`;
const errId = `${uid}-email-error`;
const cadenceName = `${uid}-cadence`;
const focusRing =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50 dark:focus-visible:ring-offset-neutral-950";
return (
<section className="relative w-full overflow-hidden bg-gradient-to-b from-neutral-50 to-neutral-100 text-neutral-900 dark:from-neutral-950 dark:to-neutral-900 dark:text-neutral-100">
<style>{`
@keyframes nxb-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .5; transform: scale(.78); } }
@keyframes nxb-float { 0%, 100% { transform: translateY(0) rotate(-6deg); } 50% { transform: translateY(-7px) rotate(-6deg); } }
@keyframes nxb-spin { to { transform: rotate(360deg); } }
.nxb-dot { animation: nxb-pulse 2.4s ease-in-out infinite; }
.nxb-stamp { animation: nxb-float 6s ease-in-out infinite; }
.nxb-spin { animation: nxb-spin .7s linear infinite; }
@media (prefers-reduced-motion: reduce) {
.nxb-dot, .nxb-stamp, .nxb-spin { animation: none !important; }
}
`}</style>
{/* atmosphere */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<div className="absolute -left-24 -top-24 h-72 w-72 rounded-full bg-amber-300/40 blur-3xl dark:bg-amber-500/10" />
<div className="absolute -right-16 top-1/3 h-80 w-80 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-500/10" />
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-neutral-300 to-transparent dark:via-neutral-700" />
</div>
<div className="relative mx-auto max-w-6xl px-6 py-20 sm:py-28">
<div className="grid grid-cols-1 gap-14 lg:grid-cols-[1.05fr_0.95fr] lg:gap-16">
{/* ── Left: the pitch ── */}
<div>
<p className="text-xs font-semibold uppercase tracking-[0.35em] text-amber-700 dark:text-amber-400">
Frontmatter
</p>
<p className="mt-4 inline-flex items-center gap-2 rounded-full border border-neutral-300/80 bg-white/60 px-3 py-1 text-xs font-medium text-neutral-600 backdrop-blur dark:border-neutral-700/80 dark:bg-neutral-900/60 dark:text-neutral-300">
<span className="nxb-dot inline-block h-1.5 w-1.5 rounded-full bg-emerald-500" />
Issue 141 ships this Sunday
</p>
<h2 className="mt-6 font-serif text-4xl leading-[1.05] tracking-tight text-neutral-900 sm:text-5xl dark:text-neutral-50">
Interface craft,
<br />
<span className="italic text-amber-700 dark:text-amber-400">one teardown</span> at a time.
</h2>
<p className="mt-6 max-w-xl text-base leading-relaxed text-neutral-600 dark:text-neutral-400">
Every Sunday, one real product interface reverse-engineered to the
pixel — the type ramp, the motion, the spacing logic — with recipes
you can paste straight into your next build. Written for people who
sweat the details nobody else notices.
</p>
<motion.ul
variants={reduce ? undefined : listContainer}
initial={reduce ? false : "hidden"}
whileInView={reduce ? undefined : "show"}
viewport={{ once: true, margin: "-12% 0px" }}
className="mt-10 grid grid-cols-1 gap-x-8 gap-y-6 sm:grid-cols-2"
>
{BENEFITS.map((b) => (
<motion.li
key={b.title}
variants={reduce ? undefined : listItem}
className="flex gap-3.5"
>
<span className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-400">
{b.icon}
</span>
<span>
<span className="block text-sm font-semibold text-neutral-900 dark:text-neutral-100">
{b.title}
</span>
<span className="mt-1 block text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
{b.body}
</span>
</span>
</motion.li>
))}
</motion.ul>
</div>
{/* ── Right: the signup card ── */}
<div className="relative">
<span
aria-hidden="true"
className="nxb-stamp absolute -right-3 -top-4 z-10 flex h-16 w-16 -rotate-6 flex-col items-center justify-center rounded-full border-2 border-dashed border-amber-500/70 bg-amber-50 text-amber-800 shadow-sm dark:bg-neutral-900 dark:text-amber-300"
>
<span className="text-[9px] font-semibold uppercase tracking-widest">Issue</span>
<span className="font-serif text-lg leading-none">141</span>
</span>
<div className="relative rounded-3xl border border-neutral-200 bg-white/80 p-7 shadow-xl shadow-neutral-900/5 backdrop-blur-sm sm:p-9 dark:border-neutral-800 dark:bg-neutral-900/70 dark:shadow-black/30">
<AnimatePresence mode="wait" initial={false}>
{status === "success" ? (
<motion.div
key="done"
initial={reduce ? false : { opacity: 0, y: 12 }}
animate={reduce ? undefined : { opacity: 1, y: 0 }}
exit={reduce ? undefined : { opacity: 0, y: -8 }}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<span className="flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-6 w-6">
<path d="m5 12.5 4.2 4L19 7" />
</svg>
</span>
<h3 className="mt-5 font-serif text-2xl text-neutral-900 dark:text-neutral-50">
You’re on the list.
</h3>
<p className="mt-3 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
The next issue lands in{" "}
<span className="font-medium text-neutral-900 dark:text-neutral-100">
{email}
</span>{" "}
{cadence === "weekly" ? "every Sunday" : "on the first of each month"}.
Check your inbox for a one-tap confirmation to lock it in.
</p>
<button
type="button"
onClick={reset}
className={`mt-6 text-sm font-semibold text-amber-700 underline decoration-amber-400/50 underline-offset-4 transition hover:decoration-amber-500 dark:text-amber-400 ${focusRing} rounded`}
>
Use a different email
</button>
</motion.div>
) : (
<motion.form
key="form"
onSubmit={onSubmit}
noValidate
initial={reduce ? false : { opacity: 0, y: 12 }}
animate={reduce ? undefined : { opacity: 1, y: 0 }}
exit={reduce ? undefined : { opacity: 0, y: -8 }}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<h3 className="font-serif text-2xl text-neutral-900 dark:text-neutral-50">
Get the next issue
</h3>
<p className="mt-2 text-sm text-neutral-600 dark:text-neutral-400">
Free forever. One click to leave, no hard feelings.
</p>
<div className="mt-6">
<label
htmlFor={emailId}
className="block text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400"
>
Email address
</label>
<input
id={emailId}
type="email"
inputMode="email"
autoComplete="email"
placeholder="you@studio.com"
value={email}
onChange={onEmailChange}
aria-invalid={error ? true : undefined}
aria-describedby={error ? errId : undefined}
className={`mt-2 w-full rounded-xl border bg-white px-4 py-3 text-sm text-neutral-900 placeholder:text-neutral-400 transition dark:bg-neutral-950 dark:text-neutral-100 dark:placeholder:text-neutral-500 ${focusRing} ${
error
? "border-rose-400 dark:border-rose-500/70"
: "border-neutral-300 hover:border-neutral-400 dark:border-neutral-700 dark:hover:border-neutral-600"
}`}
/>
{error ? (
<p
id={errId}
role="alert"
className="mt-2 flex items-center gap-1.5 text-sm text-rose-600 dark:text-rose-400"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-4 w-4">
<circle cx="12" cy="12" r="9" />
<path d="M12 7.5v5M12 16h.01" />
</svg>
{error}
</p>
) : null}
</div>
<fieldset className="mt-5">
<legend className="text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
How often?
</legend>
<div className="mt-2 grid grid-cols-2 gap-2">
{([
{ value: "weekly", label: "Every issue", note: "Weekly" },
{ value: "monthly", label: "Monthly digest", note: "12 / year" },
] as const).map((opt) => (
<label key={opt.value} className="relative cursor-pointer">
<input
type="radio"
name={cadenceName}
value={opt.value}
checked={cadence === opt.value}
onChange={() => setCadence(opt.value)}
className="peer sr-only"
/>
<span className="flex flex-col rounded-xl border border-neutral-300 bg-white px-3.5 py-2.5 text-left transition peer-checked:border-amber-500 peer-checked:bg-amber-50 peer-focus-visible:ring-2 peer-focus-visible:ring-amber-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:border-neutral-700 dark:bg-neutral-950 dark:peer-checked:border-amber-500 dark:peer-checked:bg-amber-500/10 dark:peer-focus-visible:ring-offset-neutral-900">
<span className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">
{opt.label}
</span>
<span className="text-xs text-neutral-500 dark:text-neutral-400">
{opt.note}
</span>
</span>
</label>
))}
</div>
</fieldset>
<button
type="submit"
disabled={status === "loading"}
className={`mt-6 flex w-full items-center justify-center gap-2 rounded-xl bg-neutral-900 px-5 py-3 text-sm font-semibold text-white transition hover:bg-neutral-800 disabled:cursor-not-allowed disabled:opacity-70 dark:bg-amber-400 dark:text-neutral-950 dark:hover:bg-amber-300 ${focusRing}`}
>
{status === "loading" ? (
<>
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="nxb-spin h-4 w-4">
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="3" opacity="0.25" />
<path d="M12 3a9 9 0 0 1 9 9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
</svg>
Subscribing…
</>
) : (
<>
Subscribe free
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-4 w-4">
<path d="M5 12h13m-5-5 5 5-5 5" />
</svg>
</>
)}
</button>
<div className="mt-6 flex items-center gap-3 border-t border-neutral-200 pt-5 dark:border-neutral-800">
<span aria-hidden="true" className="flex -space-x-2">
{["AK", "RM", "JL"].map((n, i) => (
<span
key={n}
className={`flex h-7 w-7 items-center justify-center rounded-full border-2 border-white text-[10px] font-semibold text-white dark:border-neutral-900 ${
["bg-indigo-500", "bg-emerald-500", "bg-rose-500"][i]
}`}
>
{n}
</span>
))}
</span>
<p className="text-xs leading-relaxed text-neutral-500 dark:text-neutral-400">
Joined by{" "}
<span className="font-semibold text-neutral-800 dark:text-neutral-200">
12,400
</span>{" "}
designers & engineers.
</p>
</div>
</motion.form>
)}
</AnimatePresence>
<p aria-live="polite" className="sr-only">
{status === "loading"
? "Submitting your subscription."
: status === "success"
? `You're on the list — a confirmation is on its way to ${email}.`
: ""}
</p>
</div>
</div>
</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 →
Inline Email Signup Bar
OriginalA wide banner that pairs a short heading with a single-line email field and subscribe button laid out side by side on desktop.

Boxed Newsletter Card
OriginalA centred, self-contained signup card with an icon badge, stacked email field and reassurance footer for dropping into a sidebar or narrow column.

Split Newsletter With Illustration
OriginalA two-column section combining copy and an inline email form with a gradient panel and hand-drawn SVG envelope illustration.

Contact Form With Details
OriginalA full contact section pairing email and location details with a name, email and message form on a bordered card.

Inline Newsletter
Originalinline newsletter bar

Card Newsletter
Originalnewsletter card

Split Image Newsletter
Originalnewsletter with image side

Gradient Newsletter
Originalgradient newsletter band

Modal Newsletter
Originalnewsletter popup card

Footer Bar Newsletter
Originalsticky footer newsletter bar

Dark Newsletter
Originaldark newsletter section

Floating Newsletter
Originalfloating newsletter widget

