CTA

CTA 02 — Newsletter Band

Full-bleed newsletter band with grid texture, corner gradient and an email capture form with privacy note.

$ npx shadcn@latest add @ludusvibe/cta-02
Open in v0

Code

Installed to components/sections/cta-02.tsx — it's yours to edit.

"use client"

import * as React from "react"

import { buttonVariants } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Reveal } from "@/components/ludus/reveal"
import { cn } from "@/lib/utils"

export type Cta02Props = {
  eyebrow?: string
  title?: React.ReactNode
  description?: string
  /** Accessible label for the email field. */
  inputLabel?: string
  placeholder?: string
  buttonLabel?: string
  privacyNote?: React.ReactNode
  /** Handle the submit yourself (e.g. call your API). Defaults to a plain `action="#"` form. */
  onSubmit?: React.FormEventHandler<HTMLFormElement>
  /** Heading element — use "h1" when this is the page's main heading. */
  as?: "h1" | "h2"
  className?: string
}

export function Cta02({
  eyebrow = "Newsletter",
  title = "Stay ahead of every release",
  description = "One email a month with product updates, practical playbooks and early access to new features. Written by the team, readable in three minutes.",
  inputLabel = "Email address",
  placeholder = "you@company.com",
  buttonLabel = "Subscribe",
  privacyNote = "No spam, ever. Unsubscribe with one click.",
  onSubmit,
  as: Heading = "h2",
  className,
}: Cta02Props) {
  return (
    <section
      className={cn("relative overflow-hidden border-y bg-background", className)}
    >
      <div
        aria-hidden
        className="absolute inset-0 [background-image:linear-gradient(to_right,var(--border)_1px,transparent_1px),linear-gradient(to_bottom,var(--border)_1px,transparent_1px)] [background-size:32px_32px] [mask-image:radial-gradient(ellipse_80%_120%_at_100%_0%,black,transparent)]"
      />
      <div
        aria-hidden
        className="absolute inset-0 bg-gradient-to-bl from-primary/10 via-transparent to-transparent"
      />

      <div className="relative mx-auto grid max-w-6xl items-center gap-10 px-6 py-20 md:py-24 lg:grid-cols-[1fr_auto] lg:gap-16">
        <div className="text-center lg:text-left">
          <Reveal>
            <p className="text-sm font-medium tracking-widest text-primary uppercase">
              {eyebrow}
            </p>
          </Reveal>
          <Reveal delay={0.1}>
            <Heading className="mt-3 text-balance font-heading text-3xl font-semibold tracking-tight md:text-4xl">
              {title}
            </Heading>
          </Reveal>
          <Reveal delay={0.2}>
            <p className="mx-auto mt-4 max-w-xl text-pretty text-muted-foreground md:text-lg lg:mx-0">
              {description}
            </p>
          </Reveal>
        </div>

        <Reveal delay={0.3} className="w-full lg:w-auto">
          <form
            action="#"
            onSubmit={onSubmit}
            className="mx-auto flex w-full max-w-md flex-col gap-3 sm:flex-row"
          >
            <Input
              type="email"
              name="email"
              required
              autoComplete="email"
              aria-label={inputLabel}
              placeholder={placeholder}
              className="h-9 flex-1 bg-background sm:min-w-64"
            />
            <button type="submit" className={buttonVariants({ size: "lg" })}>
              {buttonLabel}
            </button>
          </form>
          <p className="mt-3 text-center text-xs text-muted-foreground lg:text-left">
            {privacyNote}
          </p>
        </Reveal>
      </div>
    </section>
  )
}