Skip to content

Cta Newsletter

Centered email capture card with an indigo hairline on top: headline, copy, email field and ink button, with client validation plus pending, success and error states.

minimal/section/cta-newsletter
Open ↗

Source

"use client";

import { useId, useState, type FormEvent } from "react";
import { ArrowRight, Check } from "lucide-react";
import { cn } from "@/lib/utils";

export interface CtaNewsletterProps {
  eyebrow?: string;
  title?: string;
  description?: string;
  placeholder?: string;
  buttonLabel?: string;
  successMessage?: string;
  finePrint?: string;
  /** Called with the email; return a promise to show a pending state. Throw to show an error. */
  onSubscribe?: (email: string) => void | Promise<void>;
  className?: string;
}

/** Centered email capture card with inline validation, pending, success and error states. */
export function CtaNewsletter({
  eyebrow = "The Pager Digest",
  title = "One email a month on running calmer on-call",
  description = "Postmortems we loved, rotation patterns that work and what we shipped. Read by 18,000 engineers.",
  placeholder = "you@company.com",
  buttonLabel = "Subscribe",
  successMessage = "You’re in. Check your inbox to confirm.",
  finePrint = "No spam. Unsubscribe in one click.",
  onSubscribe,
  className,
}: CtaNewsletterProps) {
  const id = useId();
  const [email, setEmail] = useState("");
  const [state, setState] = useState<"idle" | "pending" | "done" | "error">("idle");
  const [error, setError] = useState("");

  const submit = async (e: FormEvent) => {
    e.preventDefault();
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      setState("error");
      setError("Enter a valid email address.");
      return;
    }
    setState("pending");
    try {
      await onSubscribe?.(email);
      setState("done");
    } catch {
      setState("error");
      setError("Something went wrong. Please try again.");
    }
  };

  return (
    <section className={cn("da-section bg-da-bg text-da-fg", className)}>
      <div className="da-container">
        <div className="da-stroke relative mx-auto max-w-2xl overflow-hidden rounded-da-lg bg-da-surface px-6 py-12 text-center text-da-surface-fg shadow-da-md sm:px-12">
          <div aria-hidden className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-da-primary to-transparent" />
          <p className="font-da-mono text-xs text-da-accent-fg">{eyebrow}</p>
          <h2 className="mt-3 text-2xl leading-tight font-semibold tracking-da-display text-balance sm:text-3xl">{title}</h2>
          <p className="mx-auto mt-3 max-w-md text-[15px] text-da-muted-fg">{description}</p>
          {state === "done" ? (
            <p role="status" className="mt-8 inline-flex items-center gap-2 rounded-da-pill bg-da-accent px-4 py-2 text-sm font-medium text-da-accent-fg">
              <Check aria-hidden className="size-4" /> {successMessage}
            </p>
          ) : (
            <form onSubmit={submit} noValidate className="mx-auto mt-8 flex max-w-md flex-col gap-2 sm:flex-row">
              <label htmlFor={`${id}-email`} className="sr-only">
                Email address
              </label>
              <input
                id={`${id}-email`}
                type="email"
                autoComplete="email"
                value={email}
                onChange={(e) => {
                  setEmail(e.target.value);
                  if (state === "error") setState("idle");
                }}
                placeholder={placeholder}
                aria-invalid={state === "error"}
                aria-describedby={`${id}-msg`}
                className="da-focus da-transition h-10 min-w-0 sm:flex-1 rounded-da-md border border-da-border bg-da-input px-3 text-sm placeholder:text-da-muted-fg hover:border-da-border-strong aria-invalid:border-da-danger"
              />
              <button
                type="submit"
                disabled={state === "pending"}
                className="da-focus da-transition group inline-flex h-10 items-center justify-center gap-1.5 rounded-da-md bg-da-fg px-4 text-sm font-medium text-da-bg hover:bg-da-fg/85 disabled:opacity-60"
              >
                {state === "pending" ? "Subscribing…" : buttonLabel}
                <ArrowRight aria-hidden className="da-transition size-4 group-hover:translate-x-0.5" />
              </button>
            </form>
          )}
          <p id={`${id}-msg`} aria-live="polite" className={cn("mt-3 text-[13px]", state === "error" ? "text-da-danger" : "text-da-muted-fg")}>
            {state === "error" ? error : state === "done" ? "" : finePrint}
          </p>
        </div>
      </div>
    </section>
  );
}

export default CtaNewsletter;

modules/minimal/section/cta-newsletter/index.tsx

Props

PropTypeDefaultDescription
eyebrow / title / descriptionstring—Copy.
placeholder / buttonLabel / successMessage / finePrintstring—Form texts.
onSubscribe(email: string) => void | Promise<void>—Submit handler; throw to show the error state.
classNamestring—Classes on the section.

Other cta variants in Minimal

CTA in other art directions

CTA Banner

Full-bleed yellow slab with a giant uppercase headline and an inline email capture form (pending + success states), ruled pattern in the corner.

BrutalistCTA

CTA Countdown

Limited-offer CTA in an ink slab: uppercase offer headline, yellow claim button, dashed promo code, and a four-cell live countdown (days/hours/min/sec) with the first cell in yellow.

BrutalistCTA

CTA Split

Two-audience CTA: a big headline over two fully clickable cards (yellow for engineers, blue for product teams) with audience label, title, pitch and a button chip; cards slam into their shadow on hover.

BrutalistCTA

CTA Sticker

Full-bleed pink CTA with a giant centered uppercase headline, an oversized ink button that slams into its shadow on hover, and a yellow circular text-path sticker ticking around in steps.

BrutalistCTA

Cta App

App download CTA in a glass panel: headline, ink store buttons and rating on the left; an HTML phone mock-up recording an in-person meeting (mic orb, waveform, AI chip) rising from the bottom on the right.

GlassCTA

Cta Gradient

Bold full-gradient CTA card with a masked grid overlay and top glow, white primary button, translucent secondary button and three frosted stat chips.

GlassCTA

CTA Orb

Large frosted panel with a slowly spinning, heavily blurred conic-gradient orb glowing through the glass, centered headline, ink + frosted CTAs and a reassurance line.

GlassCTA

Cta Waitlist

Frosted CTA panel haloed by a blurred gradient ring: headline, copy, pill email form with gradient button (success state) and three check perks.

GlassCTA

Cta Contact

Studio-style contact CTA: mono label, headline, the email address set huge as a link, and a row of contact channels (studio, press, careers) in mono under a hairline.

Mono CleanCTA

Cta Email

Underline-input signup: large headline, then an email field drawn as a single bottom rule with an inline “Get started →” button; confirmation replaces the form.

Mono CleanCTA

Cta Giant

Full-width CTA as a giant link: the whole headline is the link, an arrow slides in on hover and an ink underline wipes across; a mono note sits below.

Mono CleanCTA

Cta Split

Two-door CTA: the section is split into two equal halves (white | ink) with a mono label and a large title each; hovering a half inverts it and nudges the arrow.

Mono CleanCTA

Cta Band

Full-width primary-blue band with a faint grid pattern, headline + subline on the left and inverted white + outlined buttons on the right.

Neo CorporateCTA

Cta Demo

Book-a-demo CTA: benefits list on the left, bordered form card (work email, company, team size select) on the right with an inline success state.

Neo CorporateCTA

Cta Stats

Split CTA on a dark (fg-colored) panel: copy + buttons on the left, a 2×2 grid of large outcome stats divided by hairlines on the right.

Neo CorporateCTA

Cta Trust

Centered CTA inside a bordered panel: headline, two buttons, a line of compliance assurances with shield icons, and a strip of customer wordmarks below a hairline.

Neo CorporateCTA

Cta Blob

Closing CTA inside a large sage organic blob (SVG shape scaled to the content) with a clay pebble and two leaves around it; serif headline and cream pill buttons.

Organic SoftCTA

Cta Journal

Newsletter CTA styled like a paper journal: clay-tinted card with serif title, recent issue list with mono numbers, and a rounded email + button form with a warm thank-you state.

Organic SoftCTA

Cta Pledge

Interactive pledge CTA: pick the commitments your team is ready for (round check cards), a counter of companies who pledged, and a CTA that reflects how many you chose.

Organic SoftCTA

Cta Wave

Full-width sage band with soft wavy top and bottom edges (SVG), centered serif headline with an italic word, and two pill buttons; flows naturally between cream sections.

Organic SoftCTA

Cta App

App-download CTA on a peach block: headline and ink store buttons on the left, a flat white phone mock-up with a “Today” checklist (mint done items) rising from the bottom on the right.

Soft FlatCTA

Cta Block

Big periwinkle rounded block with pastel circles peeking from the corners, a centered white headline and two pill buttons (solid white and translucent).

Soft FlatCTA

Cta Illustrated

Split CTA in a white rounded card: headline, ink pill button and note on the left; a composition of pastel circle, pill, rotated square and a ✅ tile on the right.

Soft FlatCTA

Cta Newsletter

Newsletter CTA on a mint block: a flat envelope made of shapes with a notification badge, headline, pill email form and a cheerful success state.

Soft FlatCTA