# Newsletter signup

> An email signup section with pending, success and retry states. Connect your own email service to accept subscriptions.

- ID: `cmp_newsletter_signup_001`
- Slug: `newsletter-signup-01`
- Version: `1.0.0` (current)
- Status: published
- Published: 2026-09-16
- Updated: 2026-09-17
- Available versions: `1.0.0`
- Kind: section
- Primary category: `calls-to-action`
- Detail page: https://pagesugar.com/components/newsletter-signup-01
- Preview: https://pagesugar.com/preview/newsletter-signup-01

## Variants

| Variant | Label | Default | Artifact digest |
| --- | --- | --- | --- |
| `neutral` | Neutral | yes | `sha256-30fa056735b98e97bdb210b4ecf9244899ec2b14d01c7108f2e81c5757f06cfe` |

## Runtime and compatibility

- Runtime: svelte
- Svelte: 5
- SvelteKit required: no (portable Svelte component)
- Tailwind CSS: 4
- SSR: supported
- Requires client-side JavaScript: yes
- Integration level: service-required
- Appearance modes: light
- Suggested directory: `src/lib/components/newsletter-signup-01`

## Dependencies

No third-party runtime packages.

## Services

### Email subscription provider

A mailing list or email marketing service, reached through your own server endpoint, that stores subscribers and sends messages. This component only collects the address and calls onSubscribe.

Configuration:

- Implement onSubscribe to call your own endpoint (for example a SvelteKit +server.ts route or form action) that talks to the provider.
- Keep provider API keys on the server; never pass them to this component or the browser.
- Return { ok: true } on success or { ok: false, message } with a user-facing, recoverable message.

Not implemented by this component:

- Storing subscriber addresses and list membership
- Server-side email validation, rate limiting and abuse or bot protection
- Double opt-in confirmation emails where required
- Recording consent: timestamp, source and the wording the subscriber agreed to
- Unsubscribe links and processing, plus data deletion requests

## Usage

Service-required: nothing is subscribed until you implement onSubscribe. The component validates the address format with the browser, calls onSubscribe(email) once per submission, shows a pending label, then shows successMessage or the returned error message. It makes no requests of its own.

Required props: `onSubscribe`, `title`

```svelte
<!-- Implement /api/newsletter first. Replace the frequency and privacy note with your actual policy. Set successMessage to the real outcome, including double opt-in if used. -->
<script lang="ts">
	import NewsletterSignup, { type SubscribeResult } from '$lib/components/newsletter-signup-01/NewsletterSignup.svelte';

	async function subscribe(email: string): Promise<SubscribeResult> {
		const response = await fetch('/api/newsletter', {
			method: 'POST',
			headers: { 'content-type': 'application/json' },
			body: JSON.stringify({ email })
		});
		if (response.ok) return { ok: true };
		return { ok: false, message: 'We could not subscribe that address. Please try again.' };
	}
</script>

<NewsletterSignup
	title="Get product updates"
	description="One short email a month. Unsubscribe at any time."
	note="We only use your address to send this newsletter."
	onSubscribe={subscribe}
/>
```

Limitations:

- No backend: storage, double opt-in, consent records and unsubscribe handling are your provider's and server's responsibility.
- Submission requires JavaScript: the submit button stays disabled until the component hydrates, so there is no native form fallback. Add a server form action if you need one.
- Only browser-level email format validation is performed; validate again on the server.
- No consent checkbox is included; add one in the source if your jurisdiction or policy requires explicit opt-in.
- Light appearance only.

## Usage guide

### Connecting your email service

Copy `NewsletterSignup.svelte` into `src/lib/components/newsletter-signup-01/`. There are no extra runtime packages, but you must implement `onSubscribe` and connect it to a provider through your server.

The quick-start example calls `/api/newsletter`, which you must create. Keep provider credentials on the server. Replace the example mailing frequency and privacy note with your actual practices before publishing. Set `successMessage` to match the real outcome: an accepted request may still need email confirmation.

### Callback contract

```ts
onSubscribe: (email: string) => Promise<{ ok: true } | { ok: false; message: string }>;
```

- Called once per submission with the trimmed address; the input is read-only and repeat submits are
  ignored while pending.
- `{ ok: true }` shows `successMessage` and clears the input.
- `{ ok: false, message }` shows `message` (or `errorMessage` if empty) and keeps the input so the
  user can retry.
- A rejected promise shows `errorMessage`.

### What you must implement elsewhere

| Responsibility                     | Where it belongs                           |
| ---------------------------------- | ------------------------------------------ |
| Storing subscribers                | Email provider                             |
| Server-side validation, rate limit | Your endpoint                              |
| Double opt-in confirmation         | Provider (tell users via `successMessage`) |
| Consent records                    | Your endpoint / provider                   |
| Unsubscribe and deletion requests  | Provider                                   |

### Preview

The catalogue preview supplies a fake `onSubscribe` that waits briefly and returns a fixed result.
It is labelled "Demonstration — no data is sent". It is not evidence of a working integration.

## Props

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `onSubscribe` | (email: string) =\> Promise\<{ ok: true } \| { ok: false; message: string }\> | yes |  | Called with the trimmed address on submit. Resolve { ok: true } for success or { ok: false, message } to show a recoverable error. A rejected promise shows errorMessage. |
| `title` | `string` | yes |  | Section heading text. |
| `description` | `string` | no |  | Optional paragraph under the heading, e.g. what subscribers receive and how often. |
| `headingLevel` | 2 \| 3 \| 4 \| 5 | no | `2` | Level of the section heading. |
| `label` | `string` | no | `'Email address'` | Visible label for the email input. |
| `placeholder` | `string` | no | `'you@example.com'` | Input placeholder; supplementary only, never a replacement for the label. |
| `submitLabel` | `string` | no | `'Subscribe'` | Submit button text. |
| `pendingLabel` | `string` | no | `'Subscribing…'` | Submit button text while onSubscribe is pending. |
| `successMessage` | `string` | no | `'Thanks for subscribing.'` | Message shown after onSubscribe resolves { ok: true }. Mention confirmation emails here if you use double opt-in. |
| `errorMessage` | `string` | no | `'Something went wrong. Please try again.'` | Fallback error text used when onSubscribe rejects or returns an empty message. |
| `note` | `string` | no |  | Optional small print under the form, such as a privacy statement; linked to the input with aria-describedby. |

## Customization

Connect onSubscribe to your provider, adjust copy through props, and edit Tailwind classes in the source for colours and layout. No colour tokens are declared.

- Integration: implement onSubscribe to POST to your own server route; keep provider credentials on the server and map provider errors to short, recoverable messages.
- Double opt-in: if your provider sends a confirmation email, set successMessage to tell people to check their inbox.
- Consent: add a required checkbox or policy link inside the form if your policy needs explicit consent, and record it on the server.
- Copy: set title, description, label, submitLabel and note; keep the label visible.
- Colours: the card uses zinc utilities, the button bg-zinc-900 and messages text-green-800 / text-red-700; keep replacements at 4.5:1 contrast.
- Layout: the input and button stack on small screens and sit in a row from sm:; change flex-col sm:flex-row to alter this.

No public CSS variables.

## Accessibility

- The email input has a visible, programmatically associated label, type=email and autocomplete=email.
- Pending (visually hidden), success and error text is rendered in a persistent polite live region; the submit button text also changes to pendingLabel while waiting.
- While pending the button uses aria-disabled rather than disabled so keyboard focus is not lost, the input is read-only so the submitted address cannot change, and repeat submissions are ignored.
- On a failed submission the input is described by the error message (alongside the optional note) and the entered address is kept so the user can retry; aria-invalid is not set because a service failure does not mean the address is invalid.
- IDs come from $props.id(), so several signup forms on one page remain uniquely labelled.

Known limitations:

- Browser-native validation bubbles are used for empty or malformed addresses; their wording and styling vary by browser.

## License

- Declared source: MIT
- Default license approval is pending. See https://pagesugar.com/docs/license.

## Artifacts

### Neutral (`neutral`) (default)

- Artifact digest: `sha256-30fa056735b98e97bdb210b4ecf9244899ec2b14d01c7108f2e81c5757f06cfe`
- Entry: `NewsletterSignup.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_newsletter_signup_001/1.0.0/neutral/sha256-30fa056735b98e97bdb210b4ecf9244899ec2b14d01c7108f2e81c5757f06cfe/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_newsletter_signup_001/1.0.0/neutral/sha256-30fa056735b98e97bdb210b4ecf9244899ec2b14d01c7108f2e81c5757f06cfe/bundle.zip (5309 bytes, sha256 `545d8783d6e86f9c64b7d0dfa464e5abfbbe001906d38dc37b4c4c2a753676cd`)

Files:

- `NewsletterSignup.svelte` (entry, 4040 bytes): https://pagesugar.com/artifacts/cmp_newsletter_signup_001/1.0.0/neutral/sha256-30fa056735b98e97bdb210b4ecf9244899ec2b14d01c7108f2e81c5757f06cfe/source/NewsletterSignup.svelte
