# Newsletter Form

> Email newsletter form with editable CONTENT; posts to your API.

A shadcn/ui block from the [@atroui](https://www.atroui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/atroui/newsletter-form).

- Page, with a live preview: https://shadcn.exchange/r/atroui/newsletter-form
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/atroui/newsletter-form-light-29a2c066a5aa5d00.png
- Registry item: https://www.atroui.com/r/newsletter-form.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.atroui.com/r/newsletter-form.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: `lucide-react`

## Files

### `registry/default/blocks/newsletter-form.tsx`

```tsx
"use client"

import { CheckCircle2, Loader2, Mail } from "lucide-react"
import { useState, type FormEvent } from "react"

/**
 * Edit CONTENT to change copy and API endpoint.
 * Wire CONTENT.endpoint to your own route (Host API).
 */
const CONTENT = {
  placeholder: "you@company.com",
  submitLabel: "Subscribe",
  successMessage: "You're on the list - thanks!",
  endpoint: "/api/newsletter",
}

export type NewsletterFormProps = {
  className?: string
}

export function NewsletterForm({ className }: NewsletterFormProps) {
  const [email, setEmail] = useState("")
  const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">(
    "idle",
  )
  const [error, setError] = useState("")

  const onSubmit = async (e: FormEvent) => {
    e.preventDefault()
    setStatus("loading")
    setError("")
    try {
      const res = await fetch(CONTENT.endpoint, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email }),
      })
      const data = (await res.json().catch(() => ({}))) as { error?: string }
      if (!res.ok) throw new Error(data.error ?? "Failed to subscribe")
      setStatus("success")
      setEmail("")
    } catch (err) {
      setStatus("error")
      setError(err instanceof Error ? err.message : "Something went wrong")
    }
  }

  if (status === "success") {
    return (
      <div
        className={`flex items-center gap-2 text-sm text-emerald-600 dark:text-emerald-400 ${className ?? ""}`}
      >
        <CheckCircle2 className="size-4" aria-hidden />
        <span>{CONTENT.successMessage}</span>
      </div>
    )
  }

  return (
    <form
      onSubmit={onSubmit}
      className={`flex flex-col gap-2 sm:flex-row ${className ?? ""}`}
    >
      <div className="relative min-w-0 flex-1">
        <Mail
          className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground"
          aria-hidden
        />
        <input
          type="email"
          required
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder={CONTENT.placeholder}
          disabled={status === "loading"}
          className="w-full border border-border-subtle bg-background py-2.5 pr-4 pl-9 text-sm text-foreground outline-none focus:border-foreground disabled:opacity-60"
          aria-label="Email for newsletter"
        />
      </div>
      <button
        type="submit"
        disabled={status === "loading"}
        className="inline-flex h-11 shrink-0 items-center justify-center gap-2 rounded-lg bg-foreground px-5 text-sm font-medium text-background disabled:opacity-60"
      >
        {status === "loading" ? (
          <Loader2 className="size-4 animate-spin" aria-hidden />
        ) : null}
        {CONTENT.submitLabel}
      </button>
      {status === "error" ? (
        <p className="text-xs text-red-600 sm:basis-full dark:text-red-400">
          {error}
        </p>
      ) : null}
    </form>
  )
}
```
