Skip to content
    Style: Wireframe

    Blocks · Forms

    Newsletter signup

    A heading, an email field with its button attached and a note on how often you send.

    40% of the 2,073 sites in The UI Index have a form (821 sites).

    Used in UI Pro: Newsletter section

    One email a month

    What shipped, what we learned, and one deploy tip. No tracking pixels.

    Unsubscribe with one click, any time.

    Get the changelog in your inbox

    About twice a month.

    Both previews render the same markup; the HTML tab below is that markup without a framework.

    Install

    React
    npx shadcn@latest add https://www.themes.dev/ui/r/newsletter.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/newsletter.json

    Or copy the files below into your project. They need clsx, tailwind-merge and the components they import, and the themes.dev UI theme. The HTML needs only the compiled CSS.

    newsletter.classes.ts
    /** 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
    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>
      );
    }