Web InnoventixFreeCode

Newsletter CTA

Original · free

CTA with newsletter input

byWeb InnoventixReact + Tailwind
ctaxnewslettercta
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/ctax-newsletter.json
ctax-newsletter.tsx
"use client";

import { useId, useState } from "react";
import type { ChangeEvent, FormEvent } from "react";
import { motion, useReducedMotion } from "motion/react";

type Status = "idle" | "error" | "success";

const EASE = [0.22, 1, 0.36, 1] as const;

const AVATARS = [
  "from-indigo-400 to-violet-500",
  "from-sky-400 to-indigo-500",
  "from-rose-400 to-amber-400",
  "from-emerald-400 to-sky-500",
  "from-violet-400 to-rose-400",
];

const PERKS = [
  {
    label: "Every Tuesday",
    d: "M8 2v3m8-3v3M3 9h18M5 5h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Z",
  },
  {
    label: "A 5-minute read",
    d: "M12 7v5l3 2m6-2a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z",
  },
  {
    label: "One-click unsubscribe",
    d: "M5 13l4 4L19 7",
  },
];

export default function CtaxNewsletter() {
  const reduce = useReducedMotion();
  const emailId = useId();
  const noteId = useId();
  const errorId = useId();

  const [email, setEmail] = useState("");
  const [status, setStatus] = useState<Status>("idle");

  const isValidEmail = (value: string): boolean =>
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());

  const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
    event.preventDefault();
    if (!isValidEmail(email)) {
      setStatus("error");
      return;
    }
    setStatus("success");
  };

  const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
    setEmail(event.target.value);
    if (status === "error") setStatus("idle");
  };

  const reset = (): void => {
    setEmail("");
    setStatus("idle");
  };

  const container = {
    hidden: {},
    show: {
      transition: { staggerChildren: reduce ? 0 : 0.08, delayChildren: 0.04 },
    },
  };

  const item = {
    hidden: { opacity: 0, y: reduce ? 0 : 18 },
    show: {
      opacity: 1,
      y: 0,
      transition: { duration: reduce ? 0 : 0.6, ease: EASE },
    },
  };

  return (
    <section className="relative w-full overflow-hidden bg-white px-4 py-20 text-neutral-900 sm:py-28 lg:py-32 dark:bg-neutral-950 dark:text-neutral-100">
      <style>{`
        @keyframes ctaxnl-float {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
          50% { transform: translate3d(24px, -30px, 0) scale(1.08); }
        }
        @keyframes ctaxnl-drift {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
          50% { transform: translate3d(-28px, 22px, 0) scale(1.12); }
        }
        @keyframes ctaxnl-sheen {
          0% { transform: translateX(-140%) skewX(-18deg); }
          60%, 100% { transform: translateX(320%) skewX(-18deg); }
        }
        @keyframes ctaxnl-ping {
          0% { transform: scale(1); opacity: 0.7; }
          70%, 100% { transform: scale(2.4); opacity: 0; }
        }
        @keyframes ctaxnl-pop {
          0% { transform: scale(0.6); opacity: 0; }
          60% { transform: scale(1.12); }
          100% { transform: scale(1); opacity: 1; }
        }
        .ctaxnl-float { animation: ctaxnl-float 14s ease-in-out infinite; }
        .ctaxnl-drift { animation: ctaxnl-drift 18s ease-in-out infinite; }
        .ctaxnl-sheen { animation: ctaxnl-sheen 4.6s ease-in-out infinite; }
        .ctaxnl-ping { animation: ctaxnl-ping 2.4s cubic-bezier(0,0,0.2,1) infinite; }
        .ctaxnl-pop { animation: ctaxnl-pop 0.5s cubic-bezier(0.22,1,0.36,1) both; }
        @media (prefers-reduced-motion: reduce) {
          .ctaxnl-float, .ctaxnl-drift, .ctaxnl-sheen, .ctaxnl-ping, .ctaxnl-pop {
            animation: none !important;
          }
        }
      `}</style>

      {/* Decorative background */}
      <div aria-hidden className="pointer-events-none absolute inset-0 -z-10">
        <div className="ctaxnl-float absolute -left-24 -top-24 h-80 w-80 rounded-full bg-indigo-300/40 blur-3xl dark:bg-indigo-600/25" />
        <div className="ctaxnl-drift absolute -bottom-32 -right-16 h-96 w-96 rounded-full bg-violet-300/40 blur-3xl dark:bg-violet-700/25" />
        <div className="ctaxnl-float absolute right-1/3 top-1/4 h-56 w-56 rounded-full bg-sky-200/40 blur-3xl dark:bg-sky-700/20" />
        <svg
          className="absolute inset-0 h-full w-full text-neutral-900/[0.035] dark:text-white/[0.04]"
          xmlns="http://www.w3.org/2000/svg"
        >
          <defs>
            <pattern
              id="ctaxnl-grid"
              width="32"
              height="32"
              patternUnits="userSpaceOnUse"
            >
              <path d="M32 0H0V32" fill="none" stroke="currentColor" strokeWidth="1" />
            </pattern>
          </defs>
          <rect width="100%" height="100%" fill="url(#ctaxnl-grid)" />
        </svg>
      </div>

      <motion.div
        variants={container}
        initial="hidden"
        whileInView="show"
        viewport={{ once: true, amount: 0.3 }}
        className="relative mx-auto max-w-3xl"
      >
        <div className="relative overflow-hidden rounded-3xl border border-neutral-200/80 bg-neutral-50/80 p-8 shadow-xl shadow-neutral-900/5 backdrop-blur-sm sm:p-12 dark:border-white/10 dark:bg-neutral-900/70 dark:shadow-black/40">
          {/* top accent line */}
          <div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-indigo-400/70 to-transparent dark:via-violet-400/60" />

          {/* Eyebrow */}
          <motion.div variants={item} className="flex justify-center">
            <span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3.5 py-1.5 text-xs font-semibold uppercase tracking-wider text-indigo-700 dark:border-indigo-400/20 dark:bg-indigo-400/10 dark:text-indigo-300">
              <span className="relative flex h-2 w-2">
                <span className="ctaxnl-ping absolute inline-flex h-full w-full rounded-full bg-indigo-500 dark:bg-indigo-400" />
                <span className="relative inline-flex h-2 w-2 rounded-full bg-indigo-500 dark:bg-indigo-400" />
              </span>
              The Cadence Brief
            </span>
          </motion.div>

          <motion.h2
            variants={item}
            className="mt-6 text-center text-3xl font-bold tracking-tight text-balance sm:text-4xl lg:text-[2.75rem] lg:leading-[1.1]"
          >
            Ship better products,{" "}
            <span className="bg-gradient-to-r from-indigo-500 via-violet-500 to-sky-500 bg-clip-text text-transparent dark:from-indigo-400 dark:via-violet-400 dark:to-sky-400">
              one Tuesday at a time
            </span>
          </motion.h2>

          <motion.p
            variants={item}
            className="mx-auto mt-4 max-w-xl text-center text-base leading-relaxed text-neutral-600 text-pretty sm:text-lg dark:text-neutral-400"
          >
            Join 34,000+ founders, designers, and engineers reading a five-minute
            field note on the craft of building things people actually want.
            Practical, opinionated, and free.
          </motion.p>

          {/* Form / Success */}
          <motion.div variants={item} className="mx-auto mt-8 max-w-lg">
            {status === "success" ? (
              <div
                role="status"
                aria-live="polite"
                className="ctaxnl-pop flex flex-col items-center rounded-2xl border border-emerald-200 bg-emerald-50 px-6 py-8 text-center dark:border-emerald-400/20 dark:bg-emerald-400/10"
              >
                <span className="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-500 text-white shadow-lg shadow-emerald-500/30">
                  <svg
                    viewBox="0 0 24 24"
                    className="h-6 w-6"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2.5"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    aria-hidden
                  >
                    <path d="M5 13l4 4L19 7" />
                  </svg>
                </span>
                <p className="mt-4 text-lg font-semibold text-emerald-900 dark:text-emerald-200">
                  You&rsquo;re on the list.
                </p>
                <p className="mt-1 text-sm text-emerald-800/80 dark:text-emerald-300/80">
                  We just sent a confirmation to{" "}
                  <span className="font-medium">{email.trim()}</span>. Click the
                  link inside and your first brief lands this Tuesday.
                </p>
                <button
                  type="button"
                  onClick={reset}
                  className="mt-5 inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-semibold text-emerald-700 underline-offset-4 hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 dark:text-emerald-300"
                >
                  Use a different email
                </button>
              </div>
            ) : (
              <form onSubmit={handleSubmit} noValidate className="w-full">
                <label htmlFor={emailId} className="sr-only">
                  Email address
                </label>
                <div className="flex flex-col gap-3 sm:flex-row">
                  <div className="relative flex-1">
                    <span
                      aria-hidden
                      className="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400 dark:text-neutral-500"
                    >
                      <svg
                        viewBox="0 0 24 24"
                        className="h-5 w-5"
                        fill="none"
                        stroke="currentColor"
                        strokeWidth="1.75"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                      >
                        <rect x="3" y="5" width="18" height="14" rx="2" />
                        <path d="m3 7 9 6 9-6" />
                      </svg>
                    </span>
                    <input
                      id={emailId}
                      name="email"
                      type="email"
                      inputMode="email"
                      autoComplete="email"
                      placeholder="you@company.com"
                      value={email}
                      onChange={handleChange}
                      aria-invalid={status === "error"}
                      aria-describedby={
                        status === "error" ? errorId : noteId
                      }
                      className={`w-full rounded-xl border bg-white py-3.5 pl-11 pr-4 text-base text-neutral-900 shadow-sm transition-colors placeholder:text-neutral-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50 dark:bg-neutral-950/60 dark:text-neutral-100 dark:placeholder:text-neutral-500 dark:focus-visible:ring-offset-neutral-900 ${
                        status === "error"
                          ? "border-rose-400 focus-visible:ring-rose-500 dark:border-rose-500/60"
                          : "border-neutral-300 focus-visible:ring-indigo-500 dark:border-white/15"
                      }`}
                    />
                  </div>

                  <button
                    type="submit"
                    className="group relative inline-flex shrink-0 items-center justify-center gap-2 overflow-hidden rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 px-6 py-3.5 text-base font-semibold text-white shadow-lg shadow-indigo-600/25 transition-[transform,box-shadow] hover:shadow-xl hover:shadow-indigo-600/30 active:translate-y-px focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50 dark:focus-visible:ring-offset-neutral-900"
                  >
                    <span
                      aria-hidden
                      className="ctaxnl-sheen absolute inset-y-0 left-0 w-1/3 bg-white/25 blur-sm"
                    />
                    <span className="relative">Get the brief</span>
                    <svg
                      viewBox="0 0 24 24"
                      className="relative h-4.5 w-4.5 transition-transform group-hover:translate-x-0.5"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="2.25"
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      aria-hidden
                    >
                      <path d="M5 12h14M13 6l6 6-6 6" />
                    </svg>
                  </button>
                </div>

                {status === "error" ? (
                  <p
                    id={errorId}
                    role="alert"
                    className="mt-2.5 flex items-center gap-1.5 pl-1 text-sm font-medium text-rose-600 dark:text-rose-400"
                  >
                    <svg
                      viewBox="0 0 24 24"
                      className="h-4 w-4 shrink-0"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="2"
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      aria-hidden
                    >
                      <circle cx="12" cy="12" r="9" />
                      <path d="M12 8v4m0 4h.01" />
                    </svg>
                    Please enter a valid email address.
                  </p>
                ) : (
                  <p
                    id={noteId}
                    className="mt-2.5 pl-1 text-sm text-neutral-500 dark:text-neutral-500"
                  >
                    No spam, ever. Unsubscribe with a single click.
                  </p>
                )}
              </form>
            )}
          </motion.div>

          {/* Perks */}
          <motion.ul
            variants={item}
            className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-3"
          >
            {PERKS.map((perk) => (
              <li
                key={perk.label}
                className="flex items-center gap-1.5 text-sm font-medium text-neutral-600 dark:text-neutral-400"
              >
                <svg
                  viewBox="0 0 24 24"
                  className="h-4 w-4 text-indigo-500 dark:text-indigo-400"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="1.9"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  aria-hidden
                >
                  <path d={perk.d} />
                </svg>
                {perk.label}
              </li>
            ))}
          </motion.ul>

          {/* Social proof */}
          <motion.div
            variants={item}
            className="mt-8 flex flex-col items-center justify-center gap-3 border-t border-neutral-200/70 pt-6 sm:flex-row dark:border-white/10"
          >
            <div className="flex -space-x-2.5">
              {AVATARS.map((gradient, index) => (
                <span
                  key={gradient}
                  className={`inline-flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br ring-2 ring-neutral-50 dark:ring-neutral-900 ${gradient}`}
                  aria-hidden
                >
                  <span className="text-[0.65rem] font-bold text-white/90">
                    {String.fromCharCode(65 + index)}
                  </span>
                </span>
              ))}
            </div>
            <p className="text-center text-sm text-neutral-600 sm:text-left dark:text-neutral-400">
              <span className="font-semibold text-neutral-900 dark:text-neutral-100">
                34,000+ builders
              </span>{" "}
              read it every week
              <span className="ml-2 inline-flex translate-y-0.5 gap-0.5 align-middle text-amber-500">
                {Array.from({ length: 5 }).map((_, index) => (
                  <svg
                    key={index}
                    viewBox="0 0 24 24"
                    className="h-3.5 w-3.5"
                    fill="currentColor"
                    aria-hidden
                  >
                    <path d="M12 2.5l2.9 5.9 6.5.95-4.7 4.58 1.11 6.47L12 17.9l-5.81 3.06 1.11-6.47-4.7-4.58 6.5-.95L12 2.5Z" />
                  </svg>
                ))}
              </span>
            </p>
          </motion.div>
        </div>
      </motion.div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →