Newsletter
ImplementedNewsletter signup molecule (ADR-069 / plan T5): first name, last name, email, Canadian postal code, a persona choice (buyer / organizer), optional interest options and a required consent checkbox. Ships ZERO hardcoded copy — every title, label, placeholder, CTA, success/error message and the consent string is a prop. The consent box is unticked by default (Law 25 / GDPR). Used in the storefront footer modal and dedicated landing blocks.
patterns/molecules/newsletter.htmlNewsletter signup molecule — first name, last name, email, Canadian postal code, persona choice, optional interests and an unticked consent box. Zero hardcoded copy: every string is a prop.
Newsletter
Form & Choice — Family 3
Multi-field signup block. The consent checkbox is unticked by default (Law 25 / GDPR) and every user-visible string — labels, placeholders, CTA, success and validation messages — is a consumer-supplied prop.
Toggle to exercise the error path — the handler resolves { success: false, error: "…" } instead of { success: true }.
This component supports light and dark mode.
Status
Implemented — exported from the barrel. Newsletter ships the multi-field contract (first name, last name, email, Canadian postal code, persona, optional interests and an unticked consent box). The molecule ships zero hardcoded copy: every title, label, placeholder, CTA, success message, validation message and the consent string is a prop, so FR/EN is a consumer decision.
Props
title*stringHeadline above the form; also the form's accessible name.
descriptionstringOptional supporting copy under the title.
labels*NewsletterLabelsField labels — { firstName, lastName, email, postalCode, persona, interests }.
placeholderstringEmail input placeholder.
ctaLabel*stringSubmit button label.
successLabel*stringCopy announced with role="status" on success.
invalidEmailMessage*stringCopy announced when the email fails the format check.
invalidPostalCodeMessage*stringCopy announced when the postal code fails validatePostalCode.
consentRequiredMessage*stringCopy announced when the consent box is unticked.
errorMessage*stringFallback copy when the handler rejects.
consentLabel*stringConsent checkbox label (Law 25 / GDPR opt-in).
privacyLink{ href: string; label: string }Optional privacy-policy link next to the consent box.
personas*NewsletterPersonaOption[]Persona choices — buyer / organizer.
defaultPersona'buyer' | 'organizer'Initially selected persona (first option by default).
interestsNewsletterInterestOption[]Optional interest options rendered as checkboxes.
postalCodeRequiredbooleanRequire the Canadian postal code.
onSubscribe*(values: NewsletterValues) => Promise<{ success: boolean; error?: string }>Async submit handler (receives the trimmed payload).
validatePostalCode(value: string) => booleanOptional postal-code validator.
Accessibility
- The form is a labelled landmark; the four fields, the persona choice and the consent box live inside it.
- Email validation runs on submit; an invalid field gets
aria-invalid="true"and the error is announced withrole="alert". - Success announces with
role="status", a rejected submission withrole="alert"; the email input stays wired to the message througharia-describedby. - The consent checkbox is unticked by default — consent is never pre-granted (Law 25 / GDPR).
- While the handler is pending the form is
aria-busyand the submit button is disabled. - Every control meets the 44px tap minimum (
--ep-tap-min) with a visible focus ring.
Localization
The demo below renders a French variant so the i18n contract is demonstrated, not just told: the same component, different props.
Family boundaries (Form & Choice — Family 3)
- FormField (atom, implemented) — generic label+control wrapper. Newsletter composes Input / Select / Checkbox directly, so each field owns its label, error slot and aria-describedby wiring.
- FormSection / FormLayout / FormRow (molecules, implemented) — dashboard form layout chrome. No overlap: Newsletter is a single-purpose public signup block.
- Alert (molecule, implemented) — Newsletter's inline states reuse Alert's announcement semantics exactly (error →
role="alert", others →role="status"). - FooterStub (organism, implemented) — the footer's newsletter slot is the trigger that opens the modal hosting this molecule (ADR-065 / D17).
- WaitlistJoinForm (molecule, planned) — near-identical UI shape, different job: an event-scoped capacity waitlist with a sync
onJoinhandler.
Props
Newsletter
titlestring—Headline above the form; also the form's accessible name.
descriptionstring—Optional supporting copy under the title.
labelsNewsletterLabels—Field labels — { firstName, lastName, email, postalCode, persona, interests }.
placeholderstring—Email input placeholder.
ctaLabelstring—Submit button label.
successLabelstring—Copy announced with role='status' after a successful subscription.
invalidEmailMessagestring—Copy announced when the email fails the format check.
invalidPostalCodeMessagestring—Copy announced when the postal code fails validatePostalCode.
consentRequiredMessagestring—Copy announced when the consent box is still unticked on submit.
errorMessagestring—Fallback copy when onSubscribe rejects or returns no error string.
consentLabelstring—Consent checkbox label (Law 25 / GDPR opt-in).
privacyLink{ href: string; label: string }—Optional privacy-policy link rendered next to the consent box.
personasNewsletterPersonaOption[]—Persona choices — [{ value: 'buyer' | 'organizer', label }]. The first entry is the default selection.
defaultPersona'buyer' | 'organizer'First persona optionInitially selected persona.
interestsNewsletterInterestOption[]—Optional interest options — [{ id, label, checked? }] rendered as a list of checkboxes.
postalCodeRequiredbooleanFalseRequire the Canadian postal code (the footer modal asks for it).
onSubscribe(values: NewsletterValues) => Promise<{ success: boolean; error?: string }>—Async submit handler. Receives the trimmed { firstName, lastName, email, postalCode, persona, interests } payload.
validatePostalCode(value: string) => boolean—Optional postal-code validator; returning false rejects the value with invalidPostalCodeMessage.
classNamestring—Additional CSS classes on the form.
<Newsletter />Accessibility
Type
1 ruleARIA
2 rulesbehavior
3 rulesUsage
Import
import { Newsletter } from "@epasslive/design-system"Example 1
<Newsletter title="Stay in the loop" labels={labels} ctaLabel="Subscribe" successLabel="Thanks!" invalidEmailMessage="Invalid email" invalidPostalCodeMessage="Invalid postal code" consentRequiredMessage="Consent required" errorMessage="Try again" consentLabel="I agree" personas={[{ value: 'buyer', label: 'Buyer' }]} onSubscribe={subscribe} />Anti-patterns
— Do not hardcode data in molecules
— Avoid complex business logic in molecules
— Do not hardcode user-visible copy — pass it as props (bilingual by construction)
— Do not pre-tick the consent checkbox