Skip to content
ilinxa/pro-ui

Newsletter Signup

alphav0.2.0

Newsletter signup card — inline email form or CTA-only variant, async status tracking, three tones, full i18n.

Category: MarketingUpdated: 2026-08-11Created: 2026-05-02Author: ilinxa

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

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Install the component
pnpm dlx shadcn@latest add @ilinxa/newsletter-signup

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/newsletter-signup-fixtures

CLI 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

demo.tsxtsx
"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-label from labels.emailLabel; default is Email address.
  • Status region uses aria-live="polite" for success and role="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

Tags

newsletter-signupmarketingctaformsubscribemigration

Dependencies

shadcn primitives: button, input