Newsletter Footer
Original · freefooter with newsletter
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/footx-newsletter.json"use client";
import { useId, useRef, useState } from "react";
import type { FormEvent } from "react";
import { motion, useReducedMotion } from "motion/react";
type Status = "idle" | "submitting" | "success" | "error";
const TOPICS = [
{ id: "frontend", label: "Frontend" },
{ id: "backend", label: "Backend" },
{ id: "devops", label: "DevOps & SRE" },
{ id: "ai", label: "AI & ML" },
{ id: "career", label: "Career" },
] as const;
type TopicId = (typeof TOPICS)[number]["id"];
const NAV: { title: string; links: string[] }[] = [
{ title: "Product", links: ["Features", "Changelog", "Pricing", "Integrations", "Roadmap"] },
{ title: "Resources", links: ["Documentation", "Field guides", "API reference", "Community", "Status"] },
{ title: "Company", links: ["About", "Blog", "Careers", "Press kit", "Contact"] },
];
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const FOCUS =
"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:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-slate-950";
function BrandMark() {
return (
<span className="inline-flex items-center gap-2.5">
<span className="relative grid h-9 w-9 place-items-center rounded-xl bg-slate-900 dark:bg-white">
<svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
<rect x="4" y="12" width="3.2" height="8" rx="1.2" className="fill-white dark:fill-slate-900" />
<rect x="10.4" y="7" width="3.2" height="13" rx="1.2" className="fill-indigo-400" />
<rect x="16.8" y="4" width="3.2" height="16" rx="1.2" className="fill-white dark:fill-slate-900" />
</svg>
</span>
<span className="text-lg font-semibold tracking-tight text-slate-900 dark:text-white">Cadence</span>
</span>
);
}
const SOCIALS: { name: string; path: string }[] = [
{
name: "X",
path: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.653l-5.21-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.71 6.231 5.454-6.231Zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77Z",
},
{
name: "GitHub",
path: "M12 2C6.48 2 2 6.58 2 12.26c0 4.5 2.87 8.32 6.84 9.67.5.1.68-.22.68-.48 0-.24-.01-.87-.01-1.7-2.78.62-3.37-1.37-3.37-1.37-.45-1.18-1.11-1.49-1.11-1.49-.91-.63.07-.62.07-.62 1 .07 1.53 1.06 1.53 1.06.89 1.56 2.34 1.11 2.91.85.09-.66.35-1.11.63-1.37-2.22-.26-4.56-1.14-4.56-5.07 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.71 0 0 .84-.28 2.75 1.05A9.36 9.36 0 0 1 12 6.84c.85 0 1.71.12 2.51.34 1.91-1.33 2.75-1.05 2.75-1.05.55 1.41.2 2.45.1 2.71.64.72 1.03 1.63 1.03 2.75 0 3.94-2.34 4.81-4.57 5.06.36.32.68.94.68 1.9 0 1.37-.01 2.48-.01 2.82 0 .27.18.59.69.48A10.02 10.02 0 0 0 22 12.26C22 6.58 17.52 2 12 2Z",
},
{
name: "LinkedIn",
path: "M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.13 1.45-2.13 2.94v5.67H9.35V9h3.42v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.45v6.29ZM5.34 7.43a2.07 2.07 0 1 1 0-4.14 2.07 2.07 0 0 1 0 4.14ZM7.12 20.45H3.56V9h3.56v11.45ZM22.22 0H1.77C.79 0 0 .77 0 1.73v20.54C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.73V1.73C24 .77 23.2 0 22.22 0Z",
},
{
name: "RSS",
path: "M6.18 15.64a2.18 2.18 0 1 1 0 4.36 2.18 2.18 0 0 1 0-4.36ZM4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83C19.56 11.35 12.65 4.44 4 4.44Zm0 5.66v2.83a6.9 6.9 0 0 1 6.9 6.9h2.83A9.74 9.74 0 0 0 4 10.1Z",
},
];
export default function FootxNewsletter() {
const reduce = useReducedMotion();
const emailId = useId();
const errorId = useId();
const topicsHintId = useId();
const inputRef = useRef<HTMLInputElement>(null);
const [email, setEmail] = useState("");
const [frequency, setFrequency] = useState<"weekly" | "monthly">("weekly");
const [topics, setTopics] = useState<TopicId[]>(["frontend", "backend"]);
const [status, setStatus] = useState<Status>("idle");
function toggleTopic(id: TopicId) {
setTopics((prev) => (prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id]));
}
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
if (!EMAIL_RE.test(email.trim())) {
setStatus("error");
inputRef.current?.focus();
return;
}
setStatus("submitting");
window.setTimeout(() => setStatus("success"), 900);
}
function reset() {
setStatus("idle");
setEmail("");
window.setTimeout(() => inputRef.current?.focus(), 0);
}
const rise = reduce
? {}
: {
initial: { opacity: 0, y: 26 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, margin: "-90px" },
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
};
const selectedLabels = TOPICS.filter((t) => topics.includes(t.id)).map((t) => t.label);
return (
<section
aria-label="Site footer with newsletter"
className="relative w-full overflow-hidden bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100"
>
<style>{`
@keyframes footxnl-float {
0%, 100% { transform: translate3d(0,0,0) scale(1); }
50% { transform: translate3d(0,-20px,0) scale(1.07); }
}
@keyframes footxnl-pop {
0% { transform: scale(0.55); opacity: 0; }
60% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes footxnl-sheen {
0% { transform: translateX(-140%) skewX(-16deg); }
100% { transform: translateX(260%) skewX(-16deg); }
}
.footxnl-orb-a { animation: footxnl-float 17s ease-in-out infinite; }
.footxnl-orb-b { animation: footxnl-float 23s ease-in-out infinite reverse; }
.footxnl-pop { animation: footxnl-pop 460ms cubic-bezier(0.22,1,0.36,1) both; }
.footxnl-sheen { animation: footxnl-sheen 2.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.footxnl-orb-a, .footxnl-orb-b, .footxnl-pop, .footxnl-sheen { animation: none !important; }
}
`}</style>
{/* atmosphere */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<div className="footxnl-orb-a absolute -top-28 -right-16 h-80 w-80 rounded-full bg-indigo-500/15 blur-3xl dark:bg-indigo-500/25" />
<div className="footxnl-orb-b absolute -bottom-32 -left-20 h-96 w-96 rounded-full bg-violet-500/10 blur-3xl dark:bg-violet-600/20" />
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-slate-300 to-transparent dark:via-slate-700" />
</div>
<div className="relative mx-auto max-w-6xl px-6 py-20 sm:px-8 sm:py-24">
{/* ── Newsletter band ── */}
<div className="grid items-start gap-12 lg:grid-cols-2 lg:gap-16">
<motion.div {...rise}>
<span className="inline-flex items-center gap-2 rounded-full border border-slate-300/70 bg-white/60 px-3 py-1 text-xs font-medium uppercase tracking-widest text-slate-600 backdrop-blur dark:border-slate-700 dark:bg-white/5 dark:text-slate-400">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
The Cadence 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 software,
<span className="block bg-gradient-to-r from-indigo-600 to-violet-500 bg-clip-text text-transparent dark:from-indigo-400 dark:to-violet-300">
one issue at a time.
</span>
</h2>
<p className="mt-5 max-w-md text-base leading-relaxed text-slate-600 dark:text-slate-400">
A hand-edited digest of engineering essays, tooling teardowns, and honest postmortems. No
press releases, no growth-hacks — just the writing we wish landed in our own inboxes.
</p>
<div className="mt-8 flex items-center gap-4">
<div className="flex -space-x-2">
{[
{ i: "AR", c: "bg-indigo-500" },
{ i: "JK", c: "bg-emerald-500" },
{ i: "MØ", c: "bg-rose-500" },
{ i: "PL", c: "bg-amber-500" },
].map((a) => (
<span
key={a.i}
className={`grid h-8 w-8 place-items-center rounded-full text-[10px] font-semibold text-white ring-2 ring-slate-50 dark:ring-slate-950 ${a.c}`}
>
{a.i}
</span>
))}
</div>
<p className="text-sm text-slate-500 dark:text-slate-400">
Read by <span className="font-semibold text-slate-800 dark:text-slate-200">74,200+</span> engineers
</p>
</div>
</motion.div>
{/* Form card */}
<motion.div
{...rise}
className="rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-xl shadow-slate-900/5 backdrop-blur-sm sm:p-8 dark:border-slate-800 dark:bg-slate-900/60 dark:shadow-black/40"
>
{status === "success" ? (
<div role="status" aria-live="polite" className="py-4 text-center">
<span className="footxnl-pop mx-auto grid h-14 w-14 place-items-center rounded-full bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
<svg viewBox="0 0 24 24" fill="none" className="h-7 w-7" aria-hidden="true">
<path d="M20 6 9 17l-5-5" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
<h3 className="mt-5 text-xl font-semibold text-slate-900 dark:text-white">You’re on the list.</h3>
<p className="mx-auto mt-2 max-w-xs text-sm text-slate-600 dark:text-slate-400">
We sent a confirmation to{" "}
<span className="font-medium text-slate-900 dark:text-slate-200">{email.trim()}</span>. Your{" "}
<span className="font-medium text-slate-900 dark:text-slate-200">{frequency}</span> issue covers{" "}
{selectedLabels.length > 0 ? selectedLabels.join(", ") : "everything we publish"}.
</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 hover:bg-indigo-50 dark:text-indigo-400 dark:hover:bg-indigo-500/10 ${FOCUS}`}
>
Use a different email
</button>
</div>
) : (
<form onSubmit={handleSubmit} noValidate>
<label htmlFor={emailId} className="block text-sm font-medium text-slate-800 dark:text-slate-200">
Work email
</label>
<input
ref={inputRef}
id={emailId}
type="email"
inputMode="email"
autoComplete="email"
placeholder="you@company.dev"
value={email}
onChange={(e) => {
setEmail(e.target.value);
if (status === "error") setStatus("idle");
}}
aria-invalid={status === "error"}
aria-describedby={status === "error" ? errorId : undefined}
className={`mt-2 w-full rounded-xl border bg-white px-4 py-3 text-slate-900 placeholder:text-slate-400 transition dark:bg-slate-950/60 dark:text-white dark:placeholder:text-slate-500 ${FOCUS} ${
status === "error"
? "border-rose-400 dark:border-rose-500"
: "border-slate-300 dark:border-slate-700"
}`}
/>
{status === "error" && (
<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 24 24" fill="none" className="h-4 w-4 shrink-0" aria-hidden="true">
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2" />
<path d="M12 7v6M12 16.5v.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
Please enter a valid email address.
</p>
)}
{/* Frequency */}
<fieldset className="mt-6">
<legend className="text-sm font-medium text-slate-800 dark:text-slate-200">Cadence</legend>
<div className="mt-2 grid grid-cols-2 gap-2 rounded-xl border border-slate-200 bg-slate-100/70 p-1 dark:border-slate-800 dark:bg-slate-950/50">
{(["weekly", "monthly"] as const).map((f) => (
<label
key={f}
className={`relative cursor-pointer rounded-lg px-3 py-2 text-center text-sm font-medium capitalize transition ${
frequency === f
? "bg-white text-slate-900 shadow-sm dark:bg-slate-800 dark:text-white"
: "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200"
}`}
>
<input
type="radio"
name="footxnl-frequency"
value={f}
checked={frequency === f}
onChange={() => setFrequency(f)}
className="peer sr-only"
/>
<span className="pointer-events-none absolute inset-0 rounded-lg ring-2 ring-transparent peer-focus-visible:ring-indigo-500 dark:peer-focus-visible:ring-indigo-400" />
{f === "weekly" ? "Weekly" : "Monthly"}
</label>
))}
</div>
</fieldset>
{/* Topics */}
<div className="mt-6">
<p id={topicsHintId} className="text-sm font-medium text-slate-800 dark:text-slate-200">
Tune your feed
</p>
<div className="mt-2 flex flex-wrap gap-2" role="group" aria-labelledby={topicsHintId}>
{TOPICS.map((t) => {
const on = topics.includes(t.id);
return (
<button
key={t.id}
type="button"
aria-pressed={on}
onClick={() => toggleTopic(t.id)}
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm font-medium transition ${FOCUS} ${
on
? "border-indigo-500 bg-indigo-500/10 text-indigo-700 dark:border-indigo-400 dark:text-indigo-300"
: "border-slate-300 bg-transparent text-slate-600 hover:border-slate-400 dark:border-slate-700 dark:text-slate-400 dark:hover:border-slate-600"
}`}
>
<span
aria-hidden="true"
className={`grid h-4 w-4 place-items-center rounded-full border transition ${
on ? "border-indigo-500 bg-indigo-500 dark:border-indigo-400 dark:bg-indigo-400" : "border-slate-400 dark:border-slate-600"
}`}
>
{on && (
<svg viewBox="0 0 24 24" fill="none" className="h-3 w-3 text-white dark:text-slate-950">
<path d="M20 6 9 17l-5-5" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
</span>
{t.label}
</button>
);
})}
</div>
</div>
<button
type="submit"
disabled={status === "submitting"}
className={`group relative mt-7 flex w-full items-center justify-center gap-2 overflow-hidden rounded-xl bg-slate-900 px-5 py-3.5 text-sm font-semibold text-white transition hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-70 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-100 ${FOCUS}`}
>
<span className="footxnl-sheen pointer-events-none absolute inset-y-0 -left-1/2 w-1/3 bg-white/25 dark:bg-slate-900/10" aria-hidden="true" />
{status === "submitting" ? (
<>
<svg viewBox="0 0 24 24" fill="none" className="h-4 w-4 animate-spin" aria-hidden="true">
<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 for free
<svg viewBox="0 0 24 24" fill="none" className="h-4 w-4 transition-transform group-hover:translate-x-0.5" aria-hidden="true">
<path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</>
)}
</button>
<p className="mt-3 text-center text-xs text-slate-500 dark:text-slate-500">
One email per {frequency === "weekly" ? "week" : "month"}. Unsubscribe in a click. We never sell your data.
</p>
</form>
)}
</motion.div>
</div>
{/* ── Nav ── */}
<div className="mt-20 grid gap-12 border-t border-slate-200 pt-14 sm:grid-cols-2 lg:grid-cols-5 dark:border-slate-800">
<div className="lg:col-span-2">
<BrandMark />
<p className="mt-4 max-w-xs text-sm leading-relaxed text-slate-600 dark:text-slate-400">
Tools and writing for teams who care about how software gets built. Independent since 2019.
</p>
<div className="mt-6 flex gap-2">
{SOCIALS.map((s) => (
<a
key={s.name}
href="#"
target="_blank"
rel="noopener"
aria-label={`Cadence on ${s.name}`}
className={`grid h-10 w-10 place-items-center rounded-xl border border-slate-200 text-slate-500 transition hover:border-slate-300 hover:text-slate-900 dark:border-slate-800 dark:text-slate-400 dark:hover:border-slate-600 dark:hover:text-white ${FOCUS}`}
>
<svg viewBox="0 0 24 24" fill="currentColor" className="h-5 w-5" aria-hidden="true">
<path d={s.path} />
</svg>
</a>
))}
</div>
</div>
{NAV.map((col) => (
<nav key={col.title} aria-label={col.title}>
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">{col.title}</h3>
<ul className="mt-4 space-y-3">
{col.links.map((link) => (
<li key={link}>
<a
href="#"
className={`rounded text-sm text-slate-600 transition hover:text-slate-900 dark:text-slate-400 dark:hover:text-white ${FOCUS}`}
>
{link}
</a>
</li>
))}
</ul>
</nav>
))}
</div>
{/* ── Bottom bar ── */}
<div className="mt-14 flex flex-col gap-4 border-t border-slate-200 pt-8 text-sm text-slate-500 sm:flex-row sm:items-center sm:justify-between dark:border-slate-800 dark:text-slate-400">
<p>© 2026 Cadence Media, Inc. All rights reserved.</p>
<ul className="flex flex-wrap items-center gap-x-6 gap-y-2">
{["Privacy", "Terms", "Security", "Cookie settings"].map((l) => (
<li key={l}>
<a href="#" className={`rounded transition hover:text-slate-900 dark:hover:text-white ${FOCUS}`}>
{l}
</a>
</li>
))}
</ul>
</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 →
Multi Column Footer With Newsletter
OriginalA brand block with an inline email newsletter signup sits beside three navigation columns, over a bottom bar with copyright and social icons.

Minimal Single Row Footer
OriginalA compact single-row footer that centres a wordmark, inline link list and social icons above a slim copyright line.

Dark Footer With Oversized Brand
OriginalA dark footer pairing four compact link columns with an oversized gradient brand wordmark and a live status indicator.

Sitemap Style Footer
OriginalA five-column sitemap-style footer with a brand intro and contact CTA above a full site index and a legal bottom bar.

Columns Footer
Originalmulti-column footer

Minimal Footer
Originalminimal centred footer

Dark Footer
Originaldark footer with columns

CTA Top Footer
Originalfooter with a CTA on top

Social Footer
Originalfooter with social icons

App Links Footer
Originalfooter with app store links

Sitemap Footer
Originallarge sitemap footer

Gradient Footer
Originalgradient footer

