FairGarden Design

Newsletter

Email capture, as the inline footer form, the royal straddle card, a band or a ruled row. A Base UI Form with a labelled email Field (and optionally a postcode and a consent Checkbox) and a submit Button; the result renders in place, in a polite live region. Never a pop-up, a multi-step flow or a modal.

import { Newsletter } from '@fairgarden-private/design/components/Newsletter'

The email label is visible ("Email Address"); a placeholder is an example only. onSubscribe receives the values once the form validates. While busy the form is inert and the submit reads "Sending…" at its rest width, with no spinner. status="success" replaces the form with the success glyph, "Subscribed" and statusMessage, and moves focus there; status="warning" reports a server failure above the submit. At most one newsletter module per page besides the footer block.

Inline, in the footer

kind="inline" (default): the block-tier icon, the heading, the pitch and the field with the butted 64 × 48 px submit cell, which stays beside the field at every width. In a footer column it stays stacked, because it reflows on its own width.

NewsletterInline.tsx
'use client'

import * as React from 'react'
import { Ground } from '@fairgarden-private/design/components/Ground'
import { Link } from '@fairgarden-private/design/components/Link'
import {
  Newsletter,
  type NewsletterStatusKind,
} from '@fairgarden-private/design/components/Newsletter'
import styles from './inline.module.css'

/** An envelope at the block tier, drawn in currentColor (--role-accent). */
function Envelope() {
  return (
    <svg viewBox="0 0 36 36" aria-hidden="true" focusable="false" className={styles.icon}>
      <rect x="4" y="8" width="28" height="20" rx="2" />
      <path d="m5 10 13 10 13-10" />
    </svg>
  )
}

/** The inline footer form on the night band, with the butted submit cell. Submit to see "Sending…", then success. */
export function NewsletterInline() {
  const [busy, setBusy] = React.useState(false)
  const [status, setStatus] = React.useState<NewsletterStatusKind | undefined>()

  return (
    <Ground kind="band" preset="night" render={<div />} className={styles.band}>
      <div className={styles.column}>
        <Newsletter
          heading="Subscribe"
          pitch="Stories from the land, once a month."
          icon={<Envelope />}
          printUrl="example.org/newsletter"
          busy={busy}
          status={status}
          statusMessage="Check your inbox to confirm."
          legal={
            <>
              We never share your address. <Link href="https://example.org/privacy">Privacy</Link>
            </>
          }
          onSubscribe={() => {
            setBusy(true)
            window.setTimeout(() => {
              setBusy(false)
              setStatus('success')
            }, 1200)
          }}
        />
      </div>
    </Ground>
  )
}

Straddle card

Straddle card

kind="straddle" is a royal field, the page's one royal card, placed first in the band below (the footer's straddle slot, or any Ground band). It rises across that band's seam so the seam falls halfway between its heading and first field, however the heading wraps, and sits over both grounds with its one-ink edge; the lower band's content starts 56 px below it. It fills the column at base and is capped at 480 px, centered, from 768 px. With reading, on reference and editorial pages, from 1024 px it aligns to the reading column's start edge and widens to the reading measure, so Email and Postcode share a row. grained adds grain and sets the fields on white plates.

…and so the thrush returns each May to the same few acres of old forest, which is why protecting them matters.

The footer continues here.

NewsletterStraddle.tsx
'use client'

import * as React from 'react'
import { Ground } from '@fairgarden-private/design/components/Ground'
import { Link } from '@fairgarden-private/design/components/Link'
import { Newsletter } from '@fairgarden-private/design/components/Newsletter'
import styles from './straddle.module.css'

/**
 * The royal straddle card opening the night band: the seam falls between
 * its heading and first field. `reading` widens it to the reading measure
 * from 1024 px, so Email and Postcode share a row.
 */
export function NewsletterStraddle() {
  const [reading, setReading] = React.useState(true)

  return (
    <div className={styles.frame}>
      <label className={styles.toggle}>
        <input
          type="checkbox"
          checked={reading}
          onChange={(event) => setReading(event.target.checked)}
        />{' '}
        Reading page (align to the prose column)
      </label>
      <Ground kind="band" preset="paper" render={<div />} className={styles.upper}>
        <p className={styles.prose}>
          …and so the thrush returns each May to the same few acres of old forest, which is why
          protecting them matters.
        </p>
      </Ground>
      <Ground kind="band" preset="night" render={<div />} className={styles.lower}>
        <div className={styles.container}>
          <Newsletter
            kind="straddle"
            grained
            reading={reading}
            postcode
            heading="Get the Latest Bird Conservation News"
            printUrl="example.org/newsletter"
            submitLabel="Sign Up"
            legal={
              <>
                By signing up you agree to our <Link href="https://example.org/terms">terms</Link>.
              </>
            }
          />
          <p className={styles.after}>The footer continues here.</p>
        </div>
      </Ground>
    </div>
  )
}

Band and ruled row

Band and ruled row

kind="band" sets a large headline and pitch beside the field and pill: they join from 480 px of its width and the band splits 6 + 6 from 768 px. field places it in a leaf or amber campaign field. kind="ruled" is the ruled footer row: a serif prompt cell beside the input and a square submit cell, stacked below 768 px.

NewsletterBand.tsx
import { Newsletter } from '@fairgarden-private/design/components/Newsletter'
import styles from './band.module.css'

/** The band on the page ground and in a leaf campaign field, and the ruled row. The ruled row shows a server failure. */
export function NewsletterBand() {
  return (
    <div className={styles.stack}>
      <Newsletter
        kind="band"
        heading="Field Notes, Every Friday"
        pitch="One story, one walk and one thing to do this weekend."
        printUrl="example.org/notes"
      />
      <Newsletter
        kind="band"
        field="leaf"
        heading="Join the Spring Planting"
        pitch="We'll send dates and places near you."
        submitLabel="Count Me In"
        printUrl="example.org/planting"
      />
      <Newsletter
        kind="ruled"
        heading="Letters from the Orchard"
        printUrl="example.org/letters"
        status="warning"
        statusMessage="We couldn't reach the server. Try again in a minute."
      />
    </div>
  )
}

In print

The form becomes one 0.75 pt box holding the heading and "Subscribe at printUrl"; fields, legal text, grain and icon drop, and a straddle card reflows in the flow with no seam. Inside the footer the form prints nothing; the footer's imprint carries the line.

API Reference

The newsletter signup. At most one per page besides the footer block; when a page carries one, the footer shows only a link. In print it becomes one 0.75 pt box: the heading and “Subscribe at “.

PropTypeDescription
busy
boolean | undefined

While the submission is pending: the form is inert and aria-busy.

busyLabel
string | undefined

The busy label, held at the rest width. Default “Sending…".

consent
React.ReactNode | undefined

Adds an optional consent Checkbox with this label.

emailError
React.ReactNode | undefined

The error shown when the email is missing or malformed.

emailLabel
string | undefined

The email field’s visible label. Default “Email Address” (title case [D160]).

emailPlaceholder
string | undefined

A placeholder: an example only, never the label; end it with “…".

field
NewsletterBandField | undefined
grained
boolean | undefined
heading
React.ReactNode

The heading: type-itemhead (inline, straddle, ruled) or type-h2 in --role-heading (band). It names the section.

headingLevel
HeadingLevel | undefined

The heading level. Default 2.

icon
React.ReactNode | undefined

The block-tier icon before the heading (an envelope), an aria-hidden SVG in currentColor, which takes --role-accent.

kind
'inline' | 'straddle' | 'band' | 'ruled' | undefined

inline (default): the footer form, icon → heading → pitch → a field with the butted 64 × 48 px submit cell. straddle: the royal card across the night band’s seam. band: a large headline with field and pill, on the page ground or a campaign field. ruled: a ruled row of prompt, input and square submit cells.

legal
React.ReactNode | undefined

The legal or consent line, type-small at weight 600; links are Links, underlined at rest.

onSubscribe
| ((
    values: NewsletterValues,
    details: Form.SubmitEventDetails,
  ) => void)
| undefined

Called with the values once the form validates.

pitch
React.ReactNode | undefined

A one-line pitch, type-body-ui, ≤ 2 lines.

postcode
boolean | string | undefined

Adds a postcode field (true labels it “Postcode”; a string is its label). It shares Email’s row from 480 px of form width.

primary
| 'ruby'
| 'bronze'
| 'brown'
| 'crimson'
| 'gold'
| 'gray'
| 'indigo'
| 'iris'
| 'mauve'
| 'olive'
| 'pink'
| 'plum'
| 'purple'
| 'red'
| 'sage'
| 'sand'
| 'slate'
| 'tomato'
| 'violet'
| null
| undefined

Primary Radix scale: text, field edges and focus rings. Never defaulted [D133].

printUrl
string

The short URL printed in place of the form: “Subscribe at example.org/newsletter” (no protocol).

reading
boolean | undefined
secondary
| 'ruby'
| 'amber'
| 'blue'
| 'bronze'
| 'brown'
| 'crimson'
| 'cyan'
| 'gold'
| 'grass'
| 'gray'
| 'green'
| 'indigo'
| 'iris'
| 'jade'
| 'lime'
| 'mauve'
| 'mint'
| 'olive'
| 'orange'
| 'pink'
| 'plum'
| 'purple'
| 'red'
| 'sage'
| 'sand'
| 'sky'
| 'slate'
| 'teal'
| 'tomato'
| 'violet'
| 'yellow'
| null
| undefined

Secondary Radix scale: the icon, band heading and link underlines. Never defaulted.

status
NewsletterStatusKind | undefined

The result: success replaces the form with the ● glyph, “Subscribed” and statusMessage, and moves focus there; warning (a server failure) shows the ▲ glyph and statusMessage above the submit.

statusMessage
React.ReactNode | undefined

The one sentence that goes with the status.

submitLabel
string | undefined

The submit’s label (and the butted cell’s accessible name), title case, untracked. Default “Subscribe”.

successTitle
React.ReactNode | undefined

The status word for success. Default “Subscribed”.

newsletter
NewsletterBandField

The campaign fields a band newsletter may sit in [D177, D180].

type NewsletterBandField = 'leaf' | 'amber'
NewsletterProps

Props for Newsletter: section props, the kind (with its card options), the heading and copy, the fields, the submission state, the print URL and the color axes.

type NewsletterProps =
  NewsletterInlineProps | NewsletterStraddleProps | NewsletterBandProps | NewsletterRuledProps
newsletterStatus

The status part (§1.5.4): success (green) or a server-failure warning (amber).

type newsletterStatus = newsletterStatus
NewsletterStatusKind

The result of a submission, shown in place.

type NewsletterStatusKind = 'success' | 'warning'<'success' | 'warning' | null | undefined>
NewsletterValues

The values the form submits.

type NewsletterValues = { email: string; postcode?: string; consent?: boolean }

Specification: DESIGN-SYSTEM.md §11.11 (newsletter signup; the royal field per D177, reading-page width per D187) and §5.6.2 (the straddle card).