# Newsletter signup (Caseroom block)

A heading, an email field with its button attached and a note on how often you send. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

39% of the 2,062 sites in The UI Index have a form (806 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/caseroom/r/newsletter.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/newsletter.json`
- HTML: https://www.themes.dev/caseroom/r/html/newsletter.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/blocks/newsletter/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### newsletter.astro

```astro
---
import Button from '@/components/ui/button.astro';
import Input from '@/components/ui/input.astro';
import { cn } from '@/lib/utils';
import { newsletter as c, type NewsletterContent } from './newsletter.classes';

type Props = NewsletterContent & { class?: string };
const { title, description, action, button, note, variant = 'inline', id, class: className } = Astro.props;
---

<section aria-labelledby={`${id}-title`} class={cn(c.root, variant === 'band' && c.band, className)}>
  <div class={c.inner}>
    <div>
      <h2 id={`${id}-title`} class={c.title}>{title}</h2>
      {description && <p class={c.description}>{description}</p>}
    </div>
    <form action={action} method="post" class={c.form}>
      <label for={`${id}-email`} class={c.label}>Email address</label>
      <div class={c.row}>
        <Input id={`${id}-email`} name="email" type="email" required autocomplete="email" placeholder="you@example.com" aria-describedby={note ? `${id}-note` : undefined} class={c.input} />
        <Button type="submit" class={c.button}>{button}</Button>
      </div>
      {note && <p id={`${id}-note`} class={c.note}>{note}</p>}
    </form>
  </div>
</section>
```

### newsletter.classes.ts

```tsx
/** A newsletter sign-up (ported from the themes' sections/newsletter): a heading and one line, an email field with
 *  its button attached from 640px (stacked on phones), a note about how often you send. Native validation
 *  (type=email, required) with the field's own invalid style; the note is tied to the field. inline sits on the
 *  page; band is an inverted ground. */
export const newsletter = {
  root: '@container bg-background px-6 py-20 text-foreground',
  band: 'inverted',
  inner: 'mx-auto grid max-w-6xl gap-10 @3xl:grid-cols-[1.4fr_1fr] @3xl:items-end',
  title: 'text-2xl font-semibold tracking-[-0.03em] text-balance @md:text-3xl',
  description: 'mt-3 max-w-xl text-base text-pretty text-muted-foreground',
  form: 'flex w-full flex-col gap-2',
  label: 'text-sm font-medium',
  row: 'flex w-full flex-col gap-2 @md:flex-row @md:gap-0',
  input: '@md:rounded-r-none @md:border-r-0',
  button: '@md:rounded-l-none @md:h-9',
  note: 'text-sm text-muted-foreground',
};

export type NewsletterContent = {
  title: string;
  description?: string;
  /** Where the form posts. */
  action: string;
  button: string;
  note?: string;
  variant?: 'inline' | 'band';
  /** Unique on the page; ids the field and its note. */
  id: string;
};
```

### newsletter.tsx

```tsx
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { newsletter as c, type NewsletterContent } from './newsletter.classes';

export type NewsletterProps = NewsletterContent & { className?: string };

/** A newsletter sign-up: heading, one line, an email field with its button, a note tied to the field. */
export function Newsletter({ title, description, action, button, note, variant = 'inline', id, className }: NewsletterProps) {
  return (
    <section aria-labelledby={`${id}-title`} className={cn(c.root, variant === 'band' && c.band, className)}>
      <div className={c.inner}>
        <div>
          <h2 id={`${id}-title`} className={c.title}>{title}</h2>
          {description && <p className={c.description}>{description}</p>}
        </div>
        <form action={action} method="post" className={c.form}>
          <label htmlFor={`${id}-email`} className={c.label}>Email address</label>
          <div className={c.row}>
            <Input id={`${id}-email`} name="email" type="email" required autoComplete="email" placeholder="you@example.com" aria-describedby={note ? `${id}-note` : undefined} className={c.input} />
            <Button type="submit" className={c.button}>{button}</Button>
          </div>
          {note && <p id={`${id}-note`} className={c.note}>{note}</p>}
        </form>
      </div>
    </section>
  );
}
```
