Dark Newsletter
Original · freedark newsletter section
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-dark.json"use client";
import { useEffect, useRef, useState, type FormEvent } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Cadence = "weekly" | "biweekly" | "monthly";
type Status = "idle" | "submitting" | "success";
const CADENCES: { id: Cadence; label: string; hint: string }[] = [
{ id: "weekly", label: "Weekly", hint: "every Thursday" },
{ id: "biweekly", label: "Biweekly", hint: "every other Thursday" },
{ id: "monthly", label: "Monthly", hint: "on the first of the month" },
];
const TOPICS = [
"Motion",
"Design systems",
"Performance",
"Accessibility",
"CSS & layout",
"Dev tooling",
];
const ISSUES = [
{ no: "184", title: "Making view transitions feel native" },
{ no: "183", title: "The real cost of a single re-render" },
{ no: "182", title: "Focus management users can actually feel" },
];
const AVATARS: { initials: string; grad: string }[] = [
{ initials: "AK", grad: "from-indigo-400 to-violet-500" },
{ initials: "MR", grad: "from-sky-400 to-indigo-500" },
{ initials: "TL", grad: "from-violet-400 to-rose-400" },
{ initials: "JD", grad: "from-emerald-400 to-sky-500" },
];
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const styles = `
@keyframes newsxDark-rise {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes newsxDark-auroraA {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(7%, -5%, 0) scale(1.18); }
}
@keyframes newsxDark-auroraB {
0%, 100% { transform: translate3d(0, 0, 0) scale(1.12); }
50% { transform: translate3d(-6%, 6%, 0) scale(1); }
}
@keyframes newsxDark-spin { to { transform: rotate(360deg); } }
@keyframes newsxDark-draw { to { stroke-dashoffset: 0; } }
.newsxDark-auroraA { animation: newsxDark-auroraA 20s ease-in-out infinite; }
.newsxDark-auroraB { animation: newsxDark-auroraB 26s ease-in-out infinite; }
.newsxDark-spin { animation: newsxDark-spin 0.7s linear infinite; }
.newsxDark-draw { stroke-dasharray: 26; stroke-dashoffset: 26; animation: newsxDark-draw 0.5s ease 0.05s forwards; }
@media (prefers-reduced-motion: reduce) {
.newsxDark-auroraA,
.newsxDark-auroraB,
.newsxDark-spin,
.newsxDark-draw {
animation: none !important;
stroke-dashoffset: 0 !important;
}
}
`;
export default function NewsxDark() {
const reduce = useReducedMotion();
const [email, setEmail] = useState("");
const [emailError, setEmailError] = useState<string | null>(null);
const [cadence, setCadence] = useState<Cadence>("weekly");
const [topics, setTopics] = useState<string[]>(["Motion", "Performance"]);
const [status, setStatus] = useState<Status>("idle");
const inputRef = useRef<HTMLInputElement>(null);
const timeoutRef = useRef<number | null>(null);
useEffect(() => {
return () => {
if (timeoutRef.current !== null) window.clearTimeout(timeoutRef.current);
};
}, []);
function toggleTopic(topic: string) {
setTopics((prev) =>
prev.includes(topic) ? prev.filter((t) => t !== topic) : [...prev, topic],
);
}
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (status === "submitting") return;
if (!EMAIL_RE.test(email.trim())) {
setEmailError("Enter a valid email so we can reach you.");
inputRef.current?.focus();
return;
}
setEmailError(null);
setStatus("submitting");
timeoutRef.current = window.setTimeout(() => setStatus("success"), 1200);
}
function reset() {
setStatus("idle");
setEmail("");
setCadence("weekly");
setTopics(["Motion", "Performance"]);
requestAnimationFrame(() => inputRef.current?.focus());
}
const activeCadence = CADENCES.find((c) => c.id === cadence) ?? CADENCES[0];
const riseMotion = reduce
? {}
: {
initial: { opacity: 0, y: 20 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, margin: "-80px" },
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as const },
};
const swapMotion = reduce
? {}
: {
initial: { opacity: 0, y: 10 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -10 },
transition: { duration: 0.28, ease: [0.22, 1, 0.36, 1] as const },
};
const errorId = "newsxDark-email-error";
return (
<section className="relative w-full overflow-hidden bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100">
<style>{styles}</style>
{/* Atmosphere */}
<div aria-hidden className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="newsxDark-auroraA absolute -top-40 -left-24 h-[28rem] w-[28rem] rounded-full bg-indigo-300/40 blur-3xl dark:bg-indigo-600/25" />
<div className="newsxDark-auroraB absolute -bottom-48 -right-24 h-[32rem] w-[32rem] rounded-full bg-violet-300/40 blur-3xl dark:bg-violet-600/20" />
<div
className="absolute inset-0 opacity-[0.5] dark:opacity-[0.35]"
style={{
backgroundImage:
"radial-gradient(currentColor 1px, transparent 1px)",
backgroundSize: "22px 22px",
color: "rgb(100 116 139 / 0.14)",
maskImage:
"radial-gradient(ellipse 80% 60% at 50% 40%, black, transparent)",
WebkitMaskImage:
"radial-gradient(ellipse 80% 60% at 50% 40%, black, transparent)",
}}
/>
</div>
<div className="relative mx-auto max-w-6xl px-6 py-20 sm:px-8 sm:py-28">
<div className="grid items-start gap-12 lg:grid-cols-[1.05fr_1fr] lg:gap-16">
{/* Left: pitch */}
<motion.div {...riseMotion} 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-medium tracking-wide text-slate-600 backdrop-blur dark:border-white/10 dark:bg-white/5 dark:text-slate-300">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full rounded-full bg-emerald-400/70 motion-safe:animate-ping" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
</span>
The Frontier Dispatch
</span>
<h2 className="mt-6 text-4xl font-semibold leading-[1.05] tracking-tight text-slate-900 sm:text-5xl dark:text-white">
Ship better interfaces,{" "}
<span className="bg-gradient-to-r from-indigo-500 to-violet-500 bg-clip-text text-transparent dark:from-indigo-400 dark:to-violet-400">
every Thursday.
</span>
</h2>
<p className="mt-5 text-lg leading-relaxed text-slate-600 dark:text-slate-300">
A no-filler dispatch on frontend engineering, motion, and interface
design. One technique you can ship this week, worked end to end, plus
the tools and writing worth your attention.
</p>
{/* Proof */}
<div className="mt-8 flex flex-wrap items-center gap-x-6 gap-y-4">
<div className="flex items-center">
<ul className="flex -space-x-2">
{AVATARS.map((a) => (
<li
key={a.initials}
className={`grid h-9 w-9 place-items-center rounded-full bg-gradient-to-br ${a.grad} text-[0.7rem] font-semibold text-white ring-2 ring-slate-50 dark:ring-slate-950`}
>
{a.initials}
</li>
))}
</ul>
<p className="ml-3 text-sm text-slate-600 dark:text-slate-300">
<span className="font-semibold text-slate-900 dark:text-white">
24,847
</span>{" "}
engineers & designers
</p>
</div>
<div className="flex items-center gap-1.5">
<div className="flex" aria-hidden>
{Array.from({ length: 5 }).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.9.9-4.3 4.1 1 5.8L10 15l-5.2 2.7 1-5.8L1.5 7.7l5.9-.9L10 1.5z" />
</svg>
))}
</div>
<p className="text-sm text-slate-600 dark:text-slate-300">
<span className="font-semibold text-slate-900 dark:text-white">
4.9
</span>{" "}
<span className="text-slate-500 dark:text-slate-400">
from 800+ readers
</span>
</p>
</div>
</div>
{/* Recent issues */}
<div className="mt-10 rounded-2xl border border-slate-200 bg-white/60 p-5 backdrop-blur dark:border-white/10 dark:bg-white/[0.03]">
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
Recent issues
</p>
<ul className="mt-3 divide-y divide-slate-200/70 dark:divide-white/5">
{ISSUES.map((issue) => (
<li
key={issue.no}
className="flex items-center gap-3 py-2.5 text-sm"
>
<span className="shrink-0 rounded-md bg-slate-900/5 px-2 py-0.5 font-mono text-xs text-slate-500 dark:bg-white/10 dark:text-slate-400">
#{issue.no}
</span>
<span className="text-slate-700 dark:text-slate-200">
{issue.title}
</span>
</li>
))}
</ul>
</div>
</motion.div>
{/* Right: form card */}
<motion.div
{...riseMotion}
transition={
reduce
? undefined
: { duration: 0.6, delay: 0.08, ease: [0.22, 1, 0.36, 1] as const }
}
className="relative rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-xl shadow-slate-900/5 backdrop-blur-xl sm:p-8 dark:border-white/10 dark:bg-white/[0.04] dark:shadow-black/40"
>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 -top-px h-px bg-gradient-to-r from-transparent via-indigo-400/60 to-transparent"
/>
<AnimatePresence mode="wait" initial={false}>
{status === "success" ? (
<motion.div
key="success"
{...swapMotion}
role="status"
aria-live="polite"
className="flex flex-col items-start"
>
<span className="grid h-12 w-12 place-items-center rounded-2xl bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
<svg
viewBox="0 0 24 24"
className="h-6 w-6"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<path className="newsxDark-draw" d="M20 6L9 17l-5-5" />
</svg>
</span>
<h3 className="mt-5 text-2xl font-semibold tracking-tight text-slate-900 dark:text-white">
You're in. Welcome aboard.
</h3>
<p className="mt-2 text-slate-600 dark:text-slate-300">
We sent a confirmation to{" "}
<span className="font-medium text-slate-900 dark:text-white">
{email}
</span>
. Your first {activeCadence.label.toLowerCase()} issue lands{" "}
{activeCadence.hint}
{topics.length > 0
? `, tuned to ${topics.length} topic${
topics.length > 1 ? "s" : ""
}.`
: "."}
</p>
<button
type="button"
onClick={reset}
className="mt-6 inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium text-indigo-600 transition-colors hover:bg-indigo-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-indigo-300 dark:hover:bg-indigo-500/10"
>
Subscribe another email
<svg
viewBox="0 0 20 20"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4 10h12M11 5l5 5-5 5" />
</svg>
</button>
</motion.div>
) : (
<motion.form
key="form"
{...swapMotion}
onSubmit={handleSubmit}
noValidate
className="flex flex-col gap-6"
>
<div>
<h3 className="text-xl font-semibold tracking-tight text-slate-900 dark:text-white">
Start reading
</h3>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
Free, forever. Unsubscribe in one click.
</p>
</div>
{/* Email */}
<div>
<label
htmlFor="newsxDark-email"
className="block text-sm font-medium text-slate-700 dark:text-slate-200"
>
Email address
</label>
<input
ref={inputRef}
id="newsxDark-email"
type="email"
name="email"
autoComplete="email"
inputMode="email"
placeholder="you@studio.dev"
value={email}
onChange={(e) => {
setEmail(e.target.value);
if (emailError) setEmailError(null);
}}
aria-invalid={emailError ? true : undefined}
aria-describedby={emailError ? errorId : undefined}
className={`mt-2 w-full rounded-xl border bg-white px-4 py-3 text-slate-900 placeholder:text-slate-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 dark:bg-slate-900/60 dark:text-white dark:placeholder:text-slate-500 ${
emailError
? "border-rose-400 focus:border-rose-500 dark:border-rose-500/70"
: "border-slate-300 focus:border-indigo-500 dark:border-white/10 dark:focus:border-indigo-400"
}`}
/>
{emailError ? (
<p
id={errorId}
role="alert"
className="mt-2 flex items-center gap-1.5 text-sm text-rose-600 dark:text-rose-400"
>
<svg
viewBox="0 0 20 20"
className="h-4 w-4 shrink-0"
fill="currentColor"
aria-hidden
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm-.75-11.5a.75.75 0 011.5 0v4a.75.75 0 01-1.5 0v-4zM10 14a1 1 0 100-2 1 1 0 000 2z"
clipRule="evenodd"
/>
</svg>
{emailError}
</p>
) : null}
</div>
{/* Cadence */}
<fieldset>
<legend className="text-sm font-medium text-slate-700 dark:text-slate-200">
How often?
</legend>
<div className="mt-2 grid grid-cols-3 gap-2">
{CADENCES.map((c) => (
<label
key={c.id}
className="relative block cursor-pointer"
>
<input
type="radio"
name="newsxDark-cadence"
value={c.id}
checked={cadence === c.id}
onChange={() => setCadence(c.id)}
className="peer sr-only"
/>
<span className="flex h-full flex-col items-center justify-center rounded-xl border border-slate-300 bg-white px-2 py-2.5 text-center text-sm font-medium text-slate-600 transition-colors peer-checked:border-indigo-500 peer-checked:bg-indigo-50 peer-checked:text-indigo-700 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500/60 dark:border-white/10 dark:bg-slate-900/50 dark:text-slate-300 dark:peer-checked:border-indigo-400 dark:peer-checked:bg-indigo-500/15 dark:peer-checked:text-indigo-200">
{c.label}
</span>
</label>
))}
</div>
</fieldset>
{/* Topics */}
<fieldset>
<legend className="text-sm font-medium text-slate-700 dark:text-slate-200">
Tune your feed{" "}
<span className="font-normal text-slate-400 dark:text-slate-500">
(optional)
</span>
</legend>
<div className="mt-2 flex flex-wrap gap-2">
{TOPICS.map((topic) => {
const active = topics.includes(topic);
return (
<button
key={topic}
type="button"
aria-pressed={active}
onClick={() => toggleTopic(topic)}
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 ${
active
? "border-indigo-500 bg-indigo-500 text-white dark:border-indigo-400 dark:bg-indigo-500"
: "border-slate-300 bg-white text-slate-600 hover:border-slate-400 dark:border-white/10 dark:bg-slate-900/50 dark:text-slate-300 dark:hover:border-white/20"
}`}
>
<svg
viewBox="0 0 20 20"
className={`h-3.5 w-3.5 transition-transform ${
active ? "scale-100" : "scale-0"
}`}
fill="none"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M4 10l4 4 8-9" />
</svg>
{topic}
</button>
);
})}
</div>
</fieldset>
{/* Submit */}
<div>
<button
type="submit"
disabled={status === "submitting"}
className="group inline-flex w-full items-center justify-center gap-2 rounded-xl bg-slate-900 px-5 py-3.5 text-sm font-semibold text-white shadow-lg shadow-slate-900/20 transition-colors hover:bg-slate-800 focus: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-70 dark:bg-white dark:text-slate-900 dark:shadow-black/30 dark:hover:bg-slate-100 dark:focus-visible:ring-offset-slate-950"
>
{status === "submitting" ? (
<>
<svg
viewBox="0 0 24 24"
className="newsxDark-spin h-4 w-4"
fill="none"
aria-hidden
>
<circle
cx="12"
cy="12"
r="9"
stroke="currentColor"
strokeWidth="3"
className="opacity-25"
/>
<path
d="M21 12a9 9 0 00-9-9"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
/>
</svg>
Subscribing…
</>
) : (
<>
Subscribe free
<svg
viewBox="0 0 20 20"
className="h-4 w-4 transition-transform group-hover:translate-x-0.5"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M4 10h12M11 5l5 5-5 5" />
</svg>
</>
)}
</button>
<p className="mt-3 flex items-center justify-center gap-1.5 text-xs text-slate-500 dark:text-slate-400">
<svg
viewBox="0 0 20 20"
className="h-3.5 w-3.5"
fill="currentColor"
aria-hidden
>
<path
fillRule="evenodd"
d="M10 1.5l6 2.4v4.6c0 3.9-2.6 7.4-6 8.5-3.4-1.1-6-4.6-6-8.5V3.9l6-2.4zm2.9 6.4a.75.75 0 00-1.15-.96l-2.7 3.24-1.3-1.3a.75.75 0 10-1.06 1.06l1.88 1.88a.75.75 0 001.11-.06l3.22-3.86z"
clipRule="evenodd"
/>
</svg>
No spam, ever. We never share your email.
</p>
</div>
</motion.form>
)}
</AnimatePresence>
</motion.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

Floating Newsletter
Originalfloating newsletter widget

