Success Newsletter
Original · freenewsletter with success state
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-success.json"use client";
import { type FormEvent, useCallback, useEffect, useId, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Status = "idle" | "submitting" | "success";
type Cadence = "weekly" | "monthly";
interface Issue {
no: string;
title: string;
read: string;
}
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const ISSUES: Issue[] = [
{ no: "142", title: "The p99 latency trap nobody warns you about", read: "6 min" },
{ no: "141", title: "Postgres connection pooling, finally explained", read: "9 min" },
{ no: "140", title: "Why your feature flags are quietly becoming debt", read: "7 min" },
];
const CADENCE: { id: Cadence; label: string; hint: string }[] = [
{ id: "weekly", label: "Weekly digest", hint: "Every Friday, one deep-dive" },
{ id: "monthly", label: "Monthly roundup", hint: "First Monday, the best of the month" },
];
const CONFETTI = [
{ x: -132, y: -18, r: -24, d: 0, c: "#6366f1", s: 7 },
{ x: -96, y: -74, r: 40, d: 0.04, c: "#10b981", s: 9 },
{ x: -54, y: -108, r: -60, d: 0.09, c: "#f59e0b", s: 6 },
{ x: 0, y: -122, r: 12, d: 0.02, c: "#8b5cf6", s: 10 },
{ x: 54, y: -108, r: 72, d: 0.11, c: "#0ea5e9", s: 6 },
{ x: 96, y: -74, r: -36, d: 0.06, c: "#f43f5e", s: 8 },
{ x: 132, y: -18, r: 28, d: 0.03, c: "#10b981", s: 7 },
{ x: -70, y: 6, r: 54, d: 0.13, c: "#6366f1", s: 6 },
{ x: 70, y: 6, r: -48, d: 0.08, c: "#f59e0b", s: 9 },
] as const;
export default function NewsxSuccess() {
const reduce = useReducedMotion();
const uid = useId();
const emailId = `${uid}-email`;
const errorId = `${uid}-error`;
const cadenceLabelId = `${uid}-cadence`;
const liveId = `${uid}-live`;
const [status, setStatus] = useState<Status>("idle");
const [email, setEmail] = useState("");
const [cadence, setCadence] = useState<Cadence>("weekly");
const [error, setError] = useState<string | null>(null);
const [submitted, setSubmitted] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const successRef = useRef<HTMLHeadingElement>(null);
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
return () => {
if (timer.current) clearTimeout(timer.current);
};
}, []);
useEffect(() => {
if (status === "success") successRef.current?.focus();
}, [status]);
const handleSubmit = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (status === "submitting") return;
const value = email.trim();
if (value.length === 0) {
setError("Enter your email so we know where to send it.");
inputRef.current?.focus();
return;
}
if (!EMAIL_RE.test(value)) {
setError("That doesn't look like a valid email address.");
inputRef.current?.focus();
return;
}
setError(null);
setStatus("submitting");
timer.current = setTimeout(() => {
setSubmitted(value);
setStatus("success");
}, 1100);
},
[email, status],
);
const handleReset = useCallback(() => {
setStatus("idle");
setError(null);
setEmail("");
setSubmitted("");
requestAnimationFrame(() => inputRef.current?.focus());
}, []);
const cadenceCopy = CADENCE.find((c) => c.id === cadence)?.label ?? "Weekly digest";
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-5 py-20 text-slate-900 sm:px-8 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes newsx-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@keyframes newsx-shimmer {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
@keyframes newsx-check {
from { stroke-dashoffset: 34; }
to { stroke-dashoffset: 0; }
}
@keyframes newsx-ring {
0% { transform: scale(0.6); opacity: 0.8; }
100% { transform: scale(1.9); opacity: 0; }
}
.newsx-float { animation: newsx-float 5s ease-in-out infinite; }
.newsx-shimmer {
background-size: 200% 100%;
animation: newsx-shimmer 3.5s linear infinite;
}
.newsx-check-path {
stroke-dasharray: 34;
stroke-dashoffset: 34;
animation: newsx-check 0.5s 0.15s ease-out forwards;
}
.newsx-ring { animation: newsx-ring 1.1s 0.1s ease-out forwards; }
@media (prefers-reduced-motion: reduce) {
.newsx-float,
.newsx-shimmer,
.newsx-check-path,
.newsx-ring {
animation: none !important;
}
.newsx-check-path { stroke-dashoffset: 0; }
}
`}</style>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-24 left-1/2 h-72 w-[42rem] max-w-full -translate-x-1/2 rounded-full bg-gradient-to-r from-indigo-300/40 via-violet-300/30 to-sky-300/40 blur-3xl dark:from-indigo-600/20 dark:via-violet-600/15 dark:to-sky-600/20"
/>
<div className="relative mx-auto grid w-full max-w-5xl items-center gap-12 lg:grid-cols-[1.05fr_1fr] lg:gap-16">
{/* Left: pitch */}
<div className="max-w-xl">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-indigo-600 backdrop-blur dark:border-slate-800 dark:bg-slate-900/70 dark:text-indigo-300">
<span className="newsx-float relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
</span>
Issue 142 · shipping now
</span>
<h2 className="mt-5 text-3xl font-bold tracking-tight sm:text-4xl">
<span className="bg-gradient-to-r from-indigo-600 via-violet-600 to-sky-500 bg-clip-text text-transparent dark:from-indigo-300 dark:via-violet-300 dark:to-sky-300">
Deploy Friday
</span>
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-300">
A weekly letter for backend engineers who actually ship. One hard-won
production lesson, no fluff, no sponsor detours. Read in six minutes,
think about it all weekend.
</p>
<ul className="mt-8 space-y-1.5" aria-label="Recent issues">
{ISSUES.map((issue) => (
<li key={issue.no}>
<a
href="#"
className="group flex items-center gap-3 rounded-xl border border-transparent px-3 py-2.5 transition-colors hover:border-slate-200 hover:bg-white focus-visible:border-slate-200 focus-visible:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:hover:border-slate-800 dark:hover:bg-slate-900 dark:focus-visible:border-slate-800 dark:focus-visible:bg-slate-900 dark:focus-visible:ring-offset-slate-950"
>
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-slate-100 text-xs font-bold tabular-nums text-slate-500 transition-colors group-hover:bg-indigo-50 group-hover:text-indigo-600 dark:bg-slate-800 dark:text-slate-400 dark:group-hover:bg-indigo-500/15 dark:group-hover:text-indigo-300">
#{issue.no}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-slate-800 dark:text-slate-100">
{issue.title}
</span>
<span className="text-xs text-slate-400 dark:text-slate-500">
{issue.read} read
</span>
</span>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-4 w-4 shrink-0 text-slate-300 transition-transform group-hover:translate-x-0.5 group-hover:text-indigo-500 dark:text-slate-600"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</a>
</li>
))}
</ul>
<div className="mt-8 flex items-center gap-3">
<div className="flex -space-x-2" aria-hidden="true">
{["#6366f1", "#10b981", "#f59e0b", "#0ea5e9"].map((c, i) => (
<span
key={c}
className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-slate-50 text-[11px] font-bold text-white dark:border-slate-950"
style={{ backgroundColor: c }}
>
{["JR", "MK", "AL", "TS"][i]}
</span>
))}
</div>
<p className="text-sm text-slate-500 dark:text-slate-400">
Joined by{" "}
<span className="font-semibold text-slate-700 dark:text-slate-200">
12,480
</span>{" "}
engineers
</p>
</div>
</div>
{/* Right: card */}
<div className="relative">
<div className="relative overflow-hidden rounded-2xl border border-slate-200 bg-white p-6 shadow-xl shadow-slate-900/5 sm:p-8 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/40">
<div
aria-hidden="true"
className="newsx-shimmer absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-indigo-500 to-transparent"
/>
{/* polite live region for status changes */}
<p id={liveId} className="sr-only" role="status" aria-live="polite">
{status === "submitting"
? "Submitting your subscription."
: status === "success"
? `Subscribed. A confirmation link was sent to ${submitted}.`
: ""}
</p>
<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 ? undefined : { opacity: 0, y: -8 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="flex flex-col items-center py-2 text-center"
>
<div className="relative mb-5 flex h-16 w-16 items-center justify-center">
{!reduce && (
<span
aria-hidden="true"
className="newsx-ring absolute inset-0 rounded-full bg-emerald-400/40"
/>
)}
{!reduce &&
CONFETTI.map((p, i) => (
<motion.span
key={i}
aria-hidden="true"
className="absolute rounded-[2px]"
style={{
width: p.s,
height: p.s,
backgroundColor: p.c,
rotate: `${p.r}deg`,
}}
initial={{ x: 0, y: 0, scale: 0, opacity: 1 }}
animate={{ x: p.x, y: p.y, scale: 1, opacity: 0 }}
transition={{
duration: 0.9,
delay: 0.12 + p.d,
ease: "easeOut",
}}
/>
))}
<span className="relative flex h-16 w-16 items-center justify-center rounded-full bg-emerald-500 text-white shadow-lg shadow-emerald-500/30">
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-8 w-8"
fill="none"
stroke="currentColor"
strokeWidth={3}
strokeLinecap="round"
strokeLinejoin="round"
>
<path className="newsx-check-path" d="M5 13l4 4L19 7" />
</svg>
</span>
</div>
<h3
ref={successRef}
tabIndex={-1}
className="text-xl font-bold tracking-tight text-slate-900 outline-none sm:text-2xl dark:text-slate-50"
>
You're on the list.
</h3>
<p className="mt-2 max-w-xs text-sm leading-relaxed text-slate-600 dark:text-slate-300">
We sent a confirmation link to{" "}
<span className="font-semibold text-slate-900 dark:text-slate-100">
{submitted}
</span>
. Click it to lock in your{" "}
<span className="whitespace-nowrap">{cadenceCopy.toLowerCase()}</span>.
</p>
<div className="mt-6 flex w-full flex-col gap-3 sm:flex-row sm:justify-center">
<a
href="#"
className="inline-flex items-center justify-center gap-2 rounded-xl bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200 dark:focus-visible:ring-offset-slate-900"
>
Read issue #142
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</a>
<button
type="button"
onClick={handleReset}
className="inline-flex items-center justify-center rounded-xl border border-slate-200 px-4 py-2.5 text-sm font-semibold text-slate-600 transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
Use a different email
</button>
</div>
</motion.div>
) : (
<motion.div
key="form"
initial={reduce ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? undefined : { opacity: 0, y: -8 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<h3 className="text-xl font-bold tracking-tight sm:text-2xl">
Get the next issue in your inbox
</h3>
<p className="mt-1.5 text-sm text-slate-500 dark:text-slate-400">
Free forever. Unsubscribe in one click, no hard feelings.
</p>
<form onSubmit={handleSubmit} noValidate className="mt-6">
<label
htmlFor={emailId}
className="block text-sm font-medium text-slate-700 dark:text-slate-200"
>
Email address
</label>
<div className="relative mt-1.5">
<span
aria-hidden="true"
className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5 text-slate-400 dark:text-slate-500"
>
<svg
viewBox="0 0 24 24"
className="h-5 w-5"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="5" width="18" height="14" rx="2" />
<path d="M3 7l9 6 9-6" />
</svg>
</span>
<input
ref={inputRef}
id={emailId}
name="email"
type="email"
inputMode="email"
autoComplete="email"
placeholder="you@company.dev"
value={email}
disabled={status === "submitting"}
aria-invalid={error ? true : undefined}
aria-describedby={error ? errorId : undefined}
onChange={(e) => {
setEmail(e.target.value);
if (error) setError(null);
}}
className={`w-full rounded-xl border bg-white py-3 pl-11 pr-3 text-sm text-slate-900 placeholder:text-slate-400 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:opacity-60 dark:bg-slate-950 dark:text-slate-50 dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-900 ${
error
? "border-rose-400 focus-visible:ring-rose-500 dark:border-rose-500/70"
: "border-slate-300 focus-visible:ring-indigo-500 dark:border-slate-700"
}`}
/>
</div>
<AnimatePresence initial={false}>
{error && (
<motion.p
id={errorId}
role="alert"
initial={reduce ? false : { opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={reduce ? undefined : { opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
className="flex items-center gap-1.5 overflow-hidden pt-2 text-sm text-rose-600 dark:text-rose-400"
>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-4 w-4 shrink-0"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="9" />
<path d="M12 8v4M12 16h.01" />
</svg>
{error}
</motion.p>
)}
</AnimatePresence>
<fieldset className="mt-5">
<legend
id={cadenceLabelId}
className="text-sm font-medium text-slate-700 dark:text-slate-200"
>
How often?
</legend>
<div
role="radiogroup"
aria-labelledby={cadenceLabelId}
className="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2"
>
{CADENCE.map((c) => {
const active = cadence === c.id;
return (
<label
key={c.id}
className={`relative flex cursor-pointer flex-col rounded-xl border p-3 transition-colors focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 focus-within:ring-offset-white dark:focus-within:ring-offset-slate-900 ${
active
? "border-indigo-500 bg-indigo-50/70 dark:border-indigo-400/70 dark:bg-indigo-500/10"
: "border-slate-200 hover:border-slate-300 dark:border-slate-700 dark:hover:border-slate-600"
}`}
>
<input
type="radio"
name={`${uid}-cadence`}
value={c.id}
checked={active}
disabled={status === "submitting"}
onChange={() => setCadence(c.id)}
className="sr-only"
/>
<span className="flex items-center justify-between">
<span className="text-sm font-semibold text-slate-800 dark:text-slate-100">
{c.label}
</span>
<span
aria-hidden="true"
className={`flex h-4 w-4 items-center justify-center rounded-full border transition-colors ${
active
? "border-indigo-500 bg-indigo-500 dark:border-indigo-400 dark:bg-indigo-400"
: "border-slate-300 dark:border-slate-600"
}`}
>
{active && (
<span className="h-1.5 w-1.5 rounded-full bg-white" />
)}
</span>
</span>
<span className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
{c.hint}
</span>
</label>
);
})}
</div>
</fieldset>
<button
type="submit"
disabled={status === "submitting"}
className="group mt-6 flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 px-4 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-600/25 transition-all hover:from-indigo-500 hover:to-violet-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-80 dark:focus-visible:ring-offset-slate-900"
>
{status === "submitting" ? (
<>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-4 w-4 animate-spin"
fill="none"
>
<circle
cx="12"
cy="12"
r="9"
stroke="currentColor"
strokeWidth={3}
className="opacity-25"
/>
<path
d="M21 12a9 9 0 0 0-9-9"
stroke="currentColor"
strokeWidth={3}
strokeLinecap="round"
/>
</svg>
Subscribing…
</>
) : (
<>
Subscribe
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-4 w-4 transition-transform group-hover:translate-x-0.5"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</>
)}
</button>
<p className="mt-3 text-center text-xs text-slate-400 dark:text-slate-500">
No spam. We never share your address.
</p>
</form>
</motion.div>
)}
</AnimatePresence>
</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

Benefits Newsletter
Originalnewsletter with benefit bullets

Dark Newsletter
Originaldark newsletter section

