# Cta Newsletter

> CTA com captura email inline — blockus #17-24.

A shadcn/ui block from the [@lema-ds](https://ds.lema.ufpb.br) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/lema-ds/ds-cta-newsletter).

- Page, with a live preview: https://shadcn.exchange/r/lema-ds/ds-cta-newsletter
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/lema-ds/ds-cta-newsletter-light-7808401009204cb6.png
- Registry item: https://ds.lema.ufpb.br/r/ds-cta-newsletter.json
- Categories: cta
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ds.lema.ufpb.br/r/ds-cta-newsletter.json
```

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

## Dependencies

- Registry items: `@lema-ds/ds-cta`, `@lema-ds/input`, `@lema-ds/button`, `@lema-ds/ui-i18n`, `@lema-ds/ds-locale-provider`, `@lema-ds/tokens`

## Files

### `components/ds/ds-cta-newsletter.tsx`

```tsx
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import { UI_I18N, type UILocale } from "@/lib/ui-i18n"
import { useUILocale } from "@/components/ui/ds-locale-provider"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Cta, type CtaProps } from "./ds-cta"

// ── Types ──────────────────────────────────────────────────────────────────

export interface CtaNewsletterProps extends Omit<
  CtaProps,
  "primaryAction" | "secondaryAction"
> {
  onSubscribe?: (email: string) => Promise<boolean | void> | void
  placeholder?: string
  disclaimer?: string
  locale?: UILocale
}

// ── Component ──────────────────────────────────────────────────────────────

export function CtaNewsletter({
  className,
  title,
  description,
  badge,
  tone = "default",
  size = "md",
  align = "center",
  onSubscribe,
  placeholder,
  disclaimer,
  locale: localeProp,
  loading = false,
  ...props
}: CtaNewsletterProps) {
  const locale = useUILocale(localeProp)
  const [email, setEmail] = React.useState("")
  const [status, setStatus] = React.useState<
    "idle" | "loading" | "success" | "error"
  >("idle")
  const [error, setError] = React.useState<string | null>(null)

  const t = UI_I18N[locale].cta

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    const tWait = UI_I18N[locale].waitlistForm
    if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      setError(tWait.invalidEmail)
      setStatus("error")
      return
    }
    setStatus("loading")
    setError(null)
    try {
      const res = await onSubscribe?.(email)
      if (res === false) throw new Error()
      setStatus("success")
    } catch {
      setStatus("error")
      setError(tWait.invalidEmail)
    }
  }

  return (
    <div
      data-slot="cta-newsletter"
      className={cn(
        "relative overflow-hidden rounded-3xl border p-8 md:p-12",
        tone === "primary"
          ? "border-transparent bg-primary text-primary-foreground"
          : tone === "glow"
            ? "border-primary/20 bg-card shadow-xl shadow-primary/5"
            : "border-border bg-card",
        align === "center" ? "text-center" : "text-left",
        className
      )}
      {...props}
    >
      <div
        className={cn(
          "mx-auto flex max-w-2xl flex-col gap-4",
          align === "center" ? "items-center" : "items-start"
        )}
      >
        <Cta
          badge={badge}
          title={title}
          description={description}
          tone={tone === "glow" ? "default" : tone}
          align={align}
          size={size}
          loading={loading}
          className="border-0 p-0 shadow-none"
        />

        {status === "success" ? (
          <p className="text-sm font-medium text-success">Subscribed!</p>
        ) : (
          <form
            onSubmit={handleSubmit}
            className="flex w-full max-w-md flex-col gap-2 sm:flex-row"
          >
            <Input
              type="email"
              id="cta-newsletter-input"
              aria-label={placeholder ?? t.emailPlaceholder}
              aria-describedby={
                status === "error" ? "cta-newsletter-error" : undefined
              }
              placeholder={placeholder ?? t.emailPlaceholder}
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              aria-invalid={status === "error" || undefined}
              data-invalid={status === "error" || undefined}
              required
              className="h-10 flex-1 rounded-full"
            />
            <Button
              type="submit"
              disabled={status === "loading"}
              className="h-10 rounded-full px-6 font-semibold"
              variant={tone === "primary" ? "secondary" : "default"}
            >
              {status === "loading" ? "..." : t.subscribe}
            </Button>
          </form>
        )}
        {error && (
          <p
            id="cta-newsletter-error"
            role="alert"
            className="text-xs text-destructive"
          >
            {error}
          </p>
        )}
        {disclaimer ? (
          <p className="text-xs text-muted-foreground">{disclaimer}</p>
        ) : (
          <p className="text-xs text-muted-foreground">{t.noSpam}</p>
        )}
      </div>
    </div>
  )
}
```
