# Newsletter (Moonarc)

An email signup section built on a real form: native validation, an invalid state that only shows after the user has typed, a submit button whose label swaps for a spinner when the form carries data-pending, a status line for your handler, and a dot field behind. It takes a list service's form as it is (field name, hidden fields, a honeypot, form attributes) and only posts. Zero JavaScript of its own; wire the fetch yourself or let the form post.

- Pro block. Install: `npx shadcn@latest add @moonarc-pro/newsletter-section` (license key required)
- Tier B · category section · trigger scroll
- Readout: `<NewsletterSection action="/api/subscribe">`
- Browser support: widely (every browser)
- Measured cost: 0 B JS of its own · uses Reveal + runtime (with dependencies 3.0 kB raw; CSS 12.5 kB raw)
- Page: https://moonarc.dev/components/newsletter-section/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `headline` | `string` | none | Headline. |
| `action` | `string` | none | Where the form posts. Required unless demo; the build fails without it. |
| `copy` | `string` | none | Supporting copy. |
| `method` | `'post'` | `'post'` | Only post: a GET form writes the email into the URL, the history and server logs. Any other value fails the build with that rule in the message. |
| `name` | `string` | `'email'` | Name of the email field, as your list service expects it: Mailchimp's embed form posts EMAIL, Kit's email_address. |
| `placeholder` | `string` | `'you@example.com'` | Input placeholder. |
| `label` | `string` | `'Email address'` | The email field's accessible name, as its visually hidden <label>. |
| `submitLabel` | `string` | `'Subscribe'` | The submit button's label. |
| `honeypot` | `string` | none | Name of a honeypot field (Mailchimp's b_<u>_<id>): rendered display:none, tabindex -1, autocomplete off. A filled one marks a bot for your service. |
| `formAttrs` | `Record<string, string | number | boolean>` | none | Extra attributes on the <form>: target, name, id, class, a service's data-* flag, or data-ma-form and the data-msg-* texts for the core form lib. action and method fail the build here; use their props. |
| `demo` | `boolean` | `false` | A form that sends nothing: method="dialog" (goes nowhere outside a <dialog>, even without JavaScript), no action, and demoLabel shown under the form. Native validation still runs. |
| `demoLabel` | `string` | `'Demo: nothing is sent.'` | The note a demo form shows. |
| `note` | `string` | none | Small print under the form. |
| `tone` | `'surface' | 'muted' | 'ink' | 'brand'` | `unset (the card colour)` | The ground: the page surface, a muted band, an always-dark ink panel, or a deep brand panel. Each paints --background, --ma-tone-muted (over --muted, with a --border hairline), --ma-tone-ink or --ma-tone-brand (over --primary) with the matching text. With theme.css, a painted tone also re-declares --card, --border, --input and --ring inside the section, so cards and fields follow the ground. Decoration is strongest on brand. |
| `density` | `'compact' | 'cozy' | 'roomy'` | `'cozy'` | Block padding ×0.8 / ×1 / ×1.25 through --ma-density. |
| `align` | `'start' | 'center'` | `'center'` | Text alignment inside the panel. |

## Usage

```astro
---
import NewsletterSection from '@/components/moonarc/pro/NewsletterSection.astro';
---
<NewsletterSection
  headline="A monthly email with the changelog in prose"
  copy="No launch announcements, discounts or drip campaigns."
  action="/api/subscribe"
  note="Unsubscribe in one click."
/>
```

## Reduced motion

Fades in; the dots hold still; the spinner still turns, because it shows status.

## With ClientRouter

Inherits Reveal; the form is plain HTML, so nothing needs rebinding.

## Craft

- :user-invalid, not :invalid: the red border appears after the user has interacted, never on first paint.
- The spinner and the label share one grid cell, so the button does not change width while pending.
- A form that posts is the baseline; set data-pending on the form from your own fetch and the button already knows what to do, then write the outcome into the .ma-newsletter__status line (role="status"), which takes no room while empty. Or pass data-ma-form and the data-msg-* texts in formAttrs and call bindForms() from the core form lib: the form already names its status line, so the lib sets data-pending and writes those texts into it.
- Hidden fields a list service needs (a list id, a tag) go in the fields slot, inside the form.

## Replaces

- Newsletter sections (Tailwind Plus)

## Source

Pro block. The source is served by the license-gated registry: `npx shadcn@latest add @moonarc-pro/newsletter-section` with a key in components.json (https://moonarc.dev/account/). Everything it composes is free: dot-pattern, reveal, spinner.
