# Minimal Newsletter Signup

> 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.

A shadcn/ui block from the [@01orbital](https://01orbital.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/01orbital/minimal-newsletter-01).

- Page, with a live preview: https://shadcn.exchange/r/01orbital/minimal-newsletter-01
- Screenshot: https://shadcn.exchange/shot/01orbital/minimal-newsletter-01/light.png
- Registry item: https://01orbital.dev/r/minimal-newsletter-01.json
- Categories: newsletter
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/01orbital/minimal-newsletter-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@hookform/resolvers`, `react-hook-form`, `zod`
- Registry items: `@01orbital/button`, `@01orbital/field`, `@01orbital/heading`, `@01orbital/input`, `@01orbital/section`

## Files

### `blocks/newsletter/minimal-newsletter-01/index.tsx`

```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>
  )
}
```
