Skip to content

Components / Sections

Newsletter

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.

Live demo

Monthly product updates

One email a month with what shipped and what changed.

Demo: nothing is sent.

Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.

Variants · each row writes the attribute the prop writes; the section's own CSS answers

tone
density
align

Measured

JavaScript of its own

0 B

Uses Reveal and the shared runtime (1.9 kB raw, once per site). With those included: 3.0 kB raw.

CSS 12.5 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Baseline · widely available

every browser.

Install Pro

The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:

terminal
npx shadcn@latest add @moonarc-pro/newsletter-section
src/pages/index.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."
/>

Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: dot-pattern, reveal, spinner.

Props

PropTypeDefaultDescription
headlinestringnoneHeadline.
actionstringnoneWhere the form posts. Required unless demo; the build fails without it.
copystringnoneSupporting 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.
namestring'email'Name of the email field, as your list service expects it: Mailchimp's embed form posts EMAIL, Kit's email_address.
placeholderstring'you@example.com'Input placeholder.
labelstring'Email address'The email field's accessible name, as its visually hidden <label>.
submitLabelstring'Subscribe'The submit button's label.
honeypotstringnoneName 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.
formAttrsRecord<string, string | number | boolean>noneExtra 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.
demobooleanfalseA 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.
demoLabelstring'Demo: nothing is sent.'The note a demo form shows.
notestringnoneSmall 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.

Theming and overrides

The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.

Classes

.ma-newsletter.ma-newsletter__aside.ma-newsletter__busy.ma-newsletter__copy.ma-newsletter__eyebrow.ma-newsletter__form.ma-newsletter__headline.ma-newsletter__inner.ma-newsletter__label.ma-newsletter__note.ma-newsletter__sr.ma-newsletter__status

Slots

slot="eyebrow"slot="headline"slot="copy"slot="fields"slot="aside"

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.

Why it is built this way

Replaces: Newsletter sections (Tailwind Plus). See the migration table.

Dot Pattern

Backgrounds

A field of dots behind content, faded toward the edges, with an optional slow pulse.

every browseralways

Entrance on scroll that plays once and stays, staggers its children, scrubs with the scroll position when asked, honours reduced motion, and keeps working after every ClientRouter navigation.

every browserscroll

Spinner

Loading

An indeterminate arc whose length breathes while it rotates, so it looks like it is working rather than spinning a fixed wedge.

every browseralways

A closing call to action: a deep brand panel with an aurora and film grain behind it, a headline that catches a shine, a magnetic primary action, a secondary one, and a typed command.

every browserscrollpointer

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown