# Newsletter

> Email sign-up with a short pitch and privacy line. A plain HTML form you point at any provider.

- **Slug:** `newsletter`
- **Kind:** section · **Category:** Conversion
- **Source:** original to Viewership UI
- **Docs page:** https://viewership.ai/design/components/newsletter
- **All files in one bundle:** https://viewership.ai/design/components/newsletter.bundle.txt
- **Structured JSON:** https://viewership.ai/design/components/newsletter.json

## Install

1. Import the foundation tokens once (download: https://viewership.ai/design/raw/foundation.css.txt):

```astro
---
// src/layouts/Layout.astro
import "../styles/foundation.css";
---
```

2. Copy this file into `src/components/viewership/newsletter/`:

   - `Newsletter.astro` (raw: https://viewership.ai/design/raw/newsletter/Newsletter.astro.txt)

npm packages: none.

## Usage

```astro
---
import Newsletter from "../components/viewership/newsletter/Newsletter.astro";
---

<Newsletter />
```

## Example

The gallery demo, verbatim. Use the import path from Usage instead of `./`.

```astro
---
import Newsletter from "./Newsletter.astro";
---

<Newsletter
  title="Field notes,"
  accent="monthly."
  body="Trip reports, repair tips and first look at new batches. One email a month."
  action="#"
  method="get"
  note="No spam. Unsubscribe in one click."
/>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `string` |  | Headline. |
| `accent` | `string` |  | Closing words of the headline. |
| `body` | `string` |  | Short pitch. |
| `action` | `string` |  | Form endpoint (your email provider or a serverless function). |
| `method` | `"post" \| "get"` | `"post"` | Form method. |
| `field` | `string` | `"email"` | Name of the email field. |
| `placeholder` | `string` | `"you@example.com"` | Input placeholder. |
| `button` | `string` | `"Subscribe"` | Button label. |
| `note` | `string` |  | Small print under the form. |
| `as` | `"h2" \| "h3"` | `"h2"` | Heading element. |

## Source

### `src/components/viewership/newsletter/Newsletter.astro`

```astro
---
/**
 * Newsletter: an email sign-up with a short pitch and a privacy line.
 *
 * A plain HTML form: point `action` at your provider (Buttondown, ConvertKit,
 * a Netlify or Vercel function) and set `field` to the name it expects. No
 * JavaScript; the browser validates the email.
 *
 * @example
 * <Newsletter title="Field notes," accent="monthly." action="https://example.com/subscribe" />
 */
interface Props {
  title: string;
  accent?: string;
  body?: string;
  /** Form endpoint. */
  action: string;
  method?: "post" | "get";
  /** Name of the email field your provider expects. */
  field?: string;
  placeholder?: string;
  button?: string;
  /** Small print under the form. */
  note?: string;
  as?: "h2" | "h3";
  class?: string;
}

const {
  title,
  accent,
  body,
  action,
  method = "post",
  field = "email",
  placeholder = "you@example.com",
  button = "Subscribe",
  note,
  as: Heading = "h2",
  class: className,
} = Astro.props;
const id = `vs-newsletter-${Math.random().toString(36).slice(2, 8)}`;
---

<section class:list={["vs-newsletter", className]}>
  <div class="vs-newsletter__inner">
    <div class="vs-newsletter__copy">
      <Heading class="vs-display vs-newsletter__title">{title}{accent && <> <em>{accent}</em></>}</Heading>
      {body && <p class="vs-newsletter__body">{body}</p>}
    </div>
    <form class="vs-newsletter__form" action={action} method={method}>
      <label class="vs-sr-only" for={id}>Email address</label>
      <div class="vs-newsletter__field">
        <input id={id} type="email" name={field} placeholder={placeholder} autocomplete="email" required />
        <button type="submit">{button}</button>
      </div>
      {note && <p class="vs-newsletter__note">{note}</p>}
    </form>
  </div>
</section>

<style>
  .vs-newsletter {
    container-type: inline-size;
    background: var(--vs-page);
    color: var(--vs-ink);
    font-family: var(--vs-font-sans);
    font-stretch: var(--vs-font-stretch);
  }
  .vs-newsletter__inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 28px;
    max-width: var(--vs-container);
    margin-inline: auto;
    padding: var(--vs-section-y) var(--vs-gutter);
    border-top: var(--vs-border-w) solid var(--vs-line-strong);
  }
  .vs-newsletter__title {
    font-size: clamp(1.75rem, 5cqi, 3.25rem);
  }
  .vs-newsletter__body {
    max-width: 28rem;
    margin: 0.75rem 0 0;
    font-size: 1rem;
    line-height: 1.55;
    color: var(--vs-ink-2);
  }
  .vs-newsletter__field {
    display: flex;
    gap: 6px;
    padding: 6px;
    border: var(--vs-border-w) solid var(--vs-line-strong);
    border-radius: var(--vs-radius-pill);
    background: var(--vs-surface);
    box-shadow: var(--vs-pop-shadow, var(--vs-shadow-sm));
  }
  .vs-newsletter__field:focus-within {
    box-shadow: var(--vs-focus-ring);
  }
  .vs-newsletter__field input {
    flex: 1;
    width: 0;
    min-width: 0;
    padding: 0 14px;
    border: 0;
    background: transparent;
    color: var(--vs-ink);
    font: inherit;
    font-size: 0.9375rem;
  }
  .vs-newsletter__field input::placeholder {
    color: var(--vs-ink-3);
  }
  .vs-newsletter__field input:focus {
    outline: none;
  }
  .vs-newsletter__field button {
    flex: none;
    height: 40px;
    padding: 0 18px;
    border: 0;
    border-radius: var(--vs-radius-pill);
    background: var(--vs-ink);
    color: var(--vs-ink-inverse);
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    cursor: pointer;
  }
  .vs-newsletter__field button:hover {
    opacity: 0.9;
  }
  .vs-newsletter__field button:focus-visible {
    outline: 2px solid var(--vs-accent);
    outline-offset: 2px;
  }
  .vs-newsletter__note {
    margin: 12px 0 0;
    padding-left: 14px;
    font-size: 0.75rem;
    color: var(--vs-ink-3);
  }
  @container (min-width: 820px) {
    .vs-newsletter__inner {
      grid-template-columns: minmax(0, 1fr) minmax(0, 26rem);
      align-items: center;
      gap: 64px;
    }
  }
</style>
```
