Inline Newsletter
Original · freeinline newsletter bar
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-inline.json"use client";
import {
useId,
useRef,
useState,
type ChangeEvent,
type FormEvent,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Status = "idle" | "loading" | "success" | "error";
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const EASE: [number, number, number, number] = [0.22, 1, 0.36, 1];
const AVATARS: { initials: string; ring: string }[] = [
{ initials: "AK", ring: "from-indigo-500 to-violet-500" },
{ initials: "MR", ring: "from-emerald-500 to-sky-500" },
{ initials: "JP", ring: "from-amber-500 to-rose-500" },
{ initials: "SL", ring: "from-sky-500 to-indigo-500" },
];
export default function NewsxInline() {
const reduce = useReducedMotion();
const [email, setEmail] = useState("");
const [status, setStatus] = useState<Status>("idle");
const [message, setMessage] = useState("");
const [sentTo, setSentTo] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const timerRef = useRef<number | null>(null);
const helpId = useId();
const statusId = useId();
const labelId = useId();
const isLoading = status === "loading";
const isError = status === "error";
const isSuccess = status === "success";
function onChange(e: ChangeEvent<HTMLInputElement>) {
setEmail(e.target.value);
if (status === "error") {
setStatus("idle");
setMessage("");
}
}
function onSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
if (isLoading) return;
const value = email.trim();
if (!EMAIL_RE.test(value)) {
setStatus("error");
setMessage("Hmm, that email doesn't look right. Mind checking it?");
inputRef.current?.focus();
return;
}
setStatus("loading");
setMessage("");
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => {
setSentTo(value);
setStatus("success");
setMessage("You're on the list.");
}, 1100);
}
function reset() {
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
setEmail("");
setSentTo("");
setStatus("idle");
setMessage("");
}
const cardMotion = reduce
? {}
: {
initial: { opacity: 0, y: 20 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, amount: 0.4 },
transition: { duration: 0.6, ease: EASE },
};
return (
<section
className="relative w-full overflow-hidden bg-white px-4 py-16 text-slate-900 sm:py-20 dark:bg-slate-950 dark:text-slate-50"
aria-labelledby={labelId}
>
<style>{`
@keyframes nxi-spin { to { transform: rotate(360deg); } }
@keyframes nxi-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
@keyframes nxi-check {
to { stroke-dashoffset: 0; }
}
.nxi-spin { animation: nxi-spin 0.7s linear infinite; }
.nxi-shimmer {
background-size: 200% 100%;
animation: nxi-shimmer 3.2s linear infinite;
}
.nxi-check {
stroke-dasharray: 26;
stroke-dashoffset: 26;
animation: nxi-check 0.5s ease forwards 0.12s;
}
@media (prefers-reduced-motion: reduce) {
.nxi-spin, .nxi-shimmer { animation: none !important; }
.nxi-check {
animation: none !important;
stroke-dashoffset: 0 !important;
}
}
`}</style>
{/* Decorative glow — non-interactive */}
<div
aria-hidden="true"
className="pointer-events-none absolute -top-24 left-1/2 h-64 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"
/>
<motion.div
{...cardMotion}
className="relative mx-auto max-w-5xl"
>
<div className="relative overflow-hidden rounded-3xl border border-slate-200 bg-slate-50/80 shadow-xl shadow-slate-900/5 backdrop-blur-sm dark:border-slate-800 dark:bg-slate-900/70 dark:shadow-black/40">
{/* Top accent shimmer line */}
<div
aria-hidden="true"
className="nxi-shimmer h-1 w-full bg-gradient-to-r from-transparent via-indigo-500 to-transparent"
/>
<div className="flex flex-col items-start gap-8 p-6 sm:p-8 lg:flex-row lg:items-center lg:justify-between lg:gap-10 lg:p-10">
{/* Left: pitch */}
<div className="max-w-md">
<span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-indigo-700 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full rounded-full bg-indigo-500/70" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-indigo-500" />
</span>
Weekly · 5-min read
</span>
<h2
id={labelId}
className="mt-4 flex items-center gap-3 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl dark:text-white"
>
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-lg shadow-indigo-500/30">
<MailIcon className="h-5 w-5" />
</span>
The Tuesday Build
</h2>
<p className="mt-3 text-sm leading-relaxed text-slate-600 sm:text-base dark:text-slate-300">
One sharp frontend technique every Tuesday — real hooks, CSS
you'll actually ship, and the performance wins behind them.
No fluff, no recycled listicles.
</p>
<div className="mt-5 flex items-center gap-3">
<ul className="flex -space-x-2" aria-hidden="true">
{AVATARS.map((a) => (
<li
key={a.initials}
className={`flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br ${a.ring} text-[0.65rem] font-bold text-white ring-2 ring-slate-50 dark:ring-slate-900`}
>
{a.initials}
</li>
))}
</ul>
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">
Joined by{" "}
<span className="font-semibold text-slate-700 dark:text-slate-200">
14,200+
</span>{" "}
engineers
</p>
</div>
</div>
{/* Right: form / success */}
<div className="w-full lg:w-[24rem] lg:shrink-0">
<AnimatePresence
mode="wait"
initial={false}
onExitComplete={() => inputRef.current?.focus()}
>
{isSuccess ? (
<motion.div
key="success"
initial={reduce ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -8 }}
transition={{ duration: 0.3, ease: EASE }}
className="rounded-2xl border border-emerald-200 bg-emerald-50 p-5 dark:border-emerald-500/30 dark:bg-emerald-500/10"
role="status"
aria-live="polite"
>
<div className="flex items-start gap-3">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white">
<svg
viewBox="0 0 24 24"
fill="none"
className="h-5 w-5"
aria-hidden="true"
>
<path
className="nxi-check"
d="M5 13l4 4L19 7"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
<div className="min-w-0">
<p className="text-base font-semibold text-emerald-900 dark:text-emerald-100">
You're on the list.
</p>
<p className="mt-1 break-words text-sm text-emerald-800/80 dark:text-emerald-200/80">
Check your inbox — we sent a confirmation link to{" "}
<span className="font-medium">{sentTo}</span>.
</p>
<button
type="button"
onClick={reset}
className="mt-3 rounded-md text-sm font-semibold text-emerald-700 underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-50 dark:text-emerald-300 dark:focus-visible:ring-offset-slate-900"
>
Use a different email
</button>
</div>
</div>
</motion.div>
) : (
<motion.form
key="form"
onSubmit={onSubmit}
noValidate
initial={reduce ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -8 }}
transition={{ duration: 0.3, ease: EASE }}
>
<label htmlFor="nxi-email" className="sr-only">
Email address
</label>
<div className="flex flex-col gap-3 sm:flex-row">
<div className="relative flex-1">
<span className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5 text-slate-400 dark:text-slate-500">
<MailIcon className="h-5 w-5" />
</span>
<input
id="nxi-email"
ref={inputRef}
type="email"
inputMode="email"
autoComplete="email"
value={email}
onChange={onChange}
placeholder="you@company.com"
disabled={isLoading}
aria-invalid={isError}
aria-describedby={`${helpId} ${statusId}`}
className="w-full rounded-xl border border-slate-300 bg-white py-3 pl-11 pr-4 text-sm text-slate-900 placeholder:text-slate-400 focus-visible:border-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 disabled:opacity-60 aria-[invalid=true]:border-rose-400 aria-[invalid=true]:focus-visible:ring-rose-500 dark:border-slate-700 dark:bg-slate-950/60 dark:text-white dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-900"
/>
</div>
<button
type="submit"
disabled={isLoading}
className="inline-flex shrink-0 items-center justify-center gap-2 rounded-xl bg-gradient-to-br from-indigo-500 to-violet-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-500/25 transition-[transform,box-shadow] hover:-translate-y-0.5 hover:shadow-xl hover:shadow-indigo-500/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 active:translate-y-0 disabled:cursor-not-allowed disabled:opacity-80 disabled:hover:translate-y-0 dark:focus-visible:ring-offset-slate-900"
>
{isLoading ? (
<>
<svg
viewBox="0 0 24 24"
fill="none"
className="nxi-spin h-4 w-4"
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
<ArrowIcon className="h-4 w-4" />
</>
)}
</button>
</div>
<p
id={statusId}
role={isError ? "alert" : "status"}
aria-live={isError ? "assertive" : "polite"}
className={`mt-2 min-h-[1.25rem] text-xs font-medium ${
isError
? "text-rose-600 dark:text-rose-400"
: "text-transparent"
}`}
>
{isError ? message : ""}
</p>
<p
id={helpId}
className="mt-1 flex items-center gap-1.5 text-xs text-slate-500 dark:text-slate-400"
>
<LockIcon className="h-3.5 w-3.5" />
Free forever. Unsubscribe in one click.
</p>
</motion.form>
)}
</AnimatePresence>
</div>
</div>
</div>
</motion.div>
</section>
);
}
function MailIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
className={className}
aria-hidden="true"
>
<rect
x="3"
y="5"
width="18"
height="14"
rx="2.5"
stroke="currentColor"
strokeWidth={1.8}
/>
<path
d="m4 7 8 5 8-5"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function ArrowIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
className={className}
aria-hidden="true"
>
<path
d="M5 12h14m0 0-5-5m5 5-5 5"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function LockIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
className={className}
aria-hidden="true"
>
<rect
x="5"
y="11"
width="14"
height="9"
rx="2"
stroke="currentColor"
strokeWidth={1.8}
/>
<path
d="M8 11V8a4 4 0 0 1 8 0v3"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
/>
</svg>
);
}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.

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

Floating Newsletter
Originalfloating newsletter widget

