Newsletter Signup
alphav0.2.0Newsletter signup card — inline email form or CTA-only variant, async status tracking, three tones, full i18n.
Context
First component in the marketing category. Drop-in CTA for sidebars / footers / heroes asking visitors to subscribe to a newsletter. Two source variants captured: inline-form (input + button, magazine-grid sidebar) and cta-only (button-only, news-detail-page sidebar). Form state is controlled-or-uncontrolled (mirrors React input convention); status is controlled-or-derived from a Promise-returning onSubmit. Migration origin: kasder kas-social-front-v0 NewsMagazineGrid.tsx + (platform)/news/[id]/page.tsx sidebar blocks. Composed by `magazine-layout` and `detail-page-news-01` in the news-domain family.
Installation
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/newsletter-signupAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/newsletter-signup-fixturesPreview
Join our newsletter
Latest updates, straight to your inbox.
Demo source
Usage
When to use
Reach for NewsletterSignup for an email-capture CTA in a sidebar, footer, or hero — anywhere you want a brand-tinted block that asks visitors to subscribe. Two variants: inline-form (input + button) or cta-only (button-only, click leads elsewhere).
Minimal example
import { NewsletterSignup } from "@/components/newsletter-signup";
<NewsletterSignup
onSubmit={async (email) => {
await api.subscribe(email);
}}
/>;Returning a Promise from onSubmit auto-tracks the status: idle → pending → success on resolve, or idle → pending → error on reject. The card disables the input + button during pending and shows the appropriate status message.
CTA-only variant
<NewsletterSignup
variant="cta-only"
labels={{ button: "Sign up" }}
onSubmit={() => openSignupModal()}
/>;No email is captured here — the click handler typically opens a modal, navigates to a sign-up page, or triggers another flow.
Localization
<NewsletterSignup
labels={{
title: "Bültenimize Katılın",
body: "En güncel haberleri e-posta ile alın.",
placeholder: "E-posta adresiniz",
button: "Abone Ol",
successMessage: "Teşekkürler! Aboneliğiniz tamamlandı.",
errorMessage: "Bir şeyler ters gitti. Lütfen tekrar deneyin.",
}}
onSubmit={subscribeAction}
/>;Three tones
primary(default) — lime tint frame, signals the most editorial / brand-forward CTA.accent— accent-tone frame, useful when primary is already used elsewhere on the page.muted— neutral frame, good for low-noise placements like docs footers.
Controlled email value
const [email, setEmail] = useState("");
<NewsletterSignup
value={email}
onChange={setEmail}
onSubmit={async (currentEmail) => {
await api.subscribe(currentEmail);
}}
/>;Controlled status
If your form needs status from outside (e.g. a global success banner elsewhere on the page), drive the status prop:
<NewsletterSignup
status={mutation.status === "loading" ? "pending" : mutation.status === "success" ? "success" : mutation.status === "error" ? "error" : "idle"}
onSubmit={(email) => mutation.mutate(email)}
/>;Accessibility
- The form wraps input + button so Enter submits.
- Input has
aria-labelfromlabels.emailLabel; default isEmail address. - Status region uses
aria-live="polite"for success androle="alert"for error. - Button gets
aria-busy="true"during pending. - Heading semantic level via
headingAs="h2"/"h3"/"h4".
Features
- 2 visual variants — inline-form (input + button) and cta-only (full-width button only)
- 3 tones — primary (lime tint default) / accent / muted, via `tone` prop
- Controlled-or-uncontrolled email value (value + onChange OR defaultValue)
- Controlled-or-derived status — pass `status` to drive externally OR return a Promise from `onSubmit` for auto-tracking idle → pending → success/error
- Localizable — `labels` prop covers title / body / placeholder / button / success / error messages with English defaults
- Form wrapping for Enter-to-submit; button disabled + aria-busy during pending; input disabled during pending
- Status region uses aria-live=polite (success) and role=alert (error)
- Heading semantic level configurable via `headingAs` (h2 | h3 | h4)
- React.memo wrapped — prevents re-renders when used in long feeds