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 initpnpm dlx shadcn@latest add @ilinxa/newsletter-signupAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/newsletter-signup-fixturesCLI can't resolve @ilinxa? The namespace is listed in the official shadcn registry directory, so current CLIs need no configuration. If yours can't resolve it (older or pinned versions, self-hosted mirrors), register it manually in components.json:
"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}Preview
Join our newsletter
Latest updates, straight to your inbox.
Demo source
"use client"; import { Tabs, TabsContent, TabsTrigger } from "@/components/ui/tabs";import { SwipeTabsList } from "@/components/site/swipe-tabs-list";import { NewsletterSignup } from "./newsletter-signup";import { NEWSLETTER_SIGNUP_LABELS_TR, fakeSubmitError, fakeSubmitSuccess,} from "./dummy-data"; export default function NewsletterSignupDemo() { return ( <Tabs defaultValue="inline-form" className="w-full"> <SwipeTabsList> <TabsTrigger value="inline-form">Inline form</TabsTrigger> <TabsTrigger value="cta-only">CTA only</TabsTrigger> <TabsTrigger value="i18n">Custom labels</TabsTrigger> <TabsTrigger value="error">Error state</TabsTrigger> <TabsTrigger value="tones">Tones</TabsTrigger> </SwipeTabsList> <TabsContent value="inline-form" className="mt-6 max-w-md"> <NewsletterSignup onSubmit={fakeSubmitSuccess} /> </TabsContent> <TabsContent value="cta-only" className="mt-6 max-w-md"> <NewsletterSignup variant="cta-only" onSubmit={() => { window.alert("Open signup modal (demo placeholder)"); }} /> </TabsContent> <TabsContent value="i18n" className="mt-6 max-w-md"> <NewsletterSignup labels={NEWSLETTER_SIGNUP_LABELS_TR} onSubmit={fakeSubmitSuccess} /> </TabsContent> <TabsContent value="error" className="mt-6 max-w-md"> <NewsletterSignup onSubmit={fakeSubmitError} /> </TabsContent> <TabsContent value="tones" className="mt-6 grid gap-6 md:grid-cols-3"> <NewsletterSignup tone="primary" onSubmit={fakeSubmitSuccess} /> <NewsletterSignup tone="accent" onSubmit={fakeSubmitSuccess} /> <NewsletterSignup tone="muted" onSubmit={fakeSubmitSuccess} /> </TabsContent> </Tabs> );} 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