
Plain newsletter signup: a heading, a description and an email field with a subscribe button, validated with React Hook Form and Zod. Theme tokens only, light and dark.
blocks/newsletter/minimal-newsletter-01/index.tsx
"use client"
import { useId } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { z } from "zod"
import { Heading } from "@/components/editorial/heading"
import {
Section,
SectionContent,
SectionDescription,
SectionHeader,
SectionTitleGroup,
} from "@/components/editorial/section"
import { Button } from "@/components/ui/button"
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
// The copy: change text here, not in the markup.
const COPY = {
title: "Release notes, once a month",
body: "What shipped in Orbcore, what is coming next, and one long read on planning. No other mail.",
label: "Email address",
placeholder: "you@company.com",
note: "Unsubscribe from any issue with one click.",
cta: "Subscribe",
error: "Enter a valid email address.",
} as const
const schema = z.object({ email: z.email(COPY.error) })
type Values = z.infer<typeof schema>
export default function MinimalNewsletter01() {
const id = useId()
const form = useForm<Values>({
resolver: zodResolver(schema),
defaultValues: { email: "" },
})
// Your subscribe action goes here; until then the form only validates.
function onSubmit(values: Values) {
void values
}
return (
<Section>
<SectionContent width="sm">
<SectionHeader>
<SectionTitleGroup>
<Heading>{COPY.title}</Heading>
</SectionTitleGroup>
<SectionDescription>{COPY.body}</SectionDescription>
</SectionHeader>
<form onSubmit={form.handleSubmit(onSubmit)} noValidate>
<Controller
name="email"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={`${id}-email`} className="sr-only">
{COPY.label}
</FieldLabel>
<div className="flex flex-col gap-3 @sm/section:flex-row">
<Input
{...field}
id={`${id}-email`}
type="email"
autoComplete="email"
placeholder={COPY.placeholder}
required
aria-invalid={fieldState.invalid}
className="flex-1"
/>
<Button type="submit">{COPY.cta}</Button>
</div>
<FieldDescription>{COPY.note}</FieldDescription>
<FieldError errors={[fieldState.error]} />
</Field>
)}
/>
</form>
</SectionContent>
</Section>
)
}