Web InnoventixFreeCode

Card CTA

Original · free

boxed CTA card

byWeb InnoventixReact + Tailwind
ctaxcardcta
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-card.json
ctax-card.tsx
"use client";

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

type Audience = "teams" | "solo";

const AUDIENCE_COPY: Record<
  Audience,
  { headline: string; sub: string; bullet: string }
> = {
  teams: {
    headline: "Give your team one place to see the whole picture.",
    sub: "Beacon pulls tasks, docs, and status updates into a single live workspace — so no one has to ask “where are we on this?” across three different apps.",
    bullet: "Unlimited members on every plan",
  },
  solo: {
    headline: "Everything you're juggling, finally in one calm view.",
    sub: "Beacon keeps your projects, notes, and deadlines in one workspace you'll actually want to open every morning — no setup marathon required.",
    bullet: "Ready to use in under two minutes",
  },
};

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

const AVATARS: { initials: string; from: string; to: string }[] = [
  { initials: "AR", from: "from-rose-400", to: "to-amber-400" },
  { initials: "MK", from: "from-sky-400", to: "to-indigo-500" },
  { initials: "JL", from: "from-emerald-400", to: "to-teal-500" },
  { initials: "TN", from: "from-violet-400", to: "to-fuchsia-500" },
];

export default function CtaxCard() {
  const reduce = useReducedMotion();
  const [audience, setAudience] = useState<Audience>("teams");
  const [email, setEmail] = useState("");
  const [status, setStatus] = useState<"idle" | "error" | "success">("idle");

  const emailId = useId();
  const errorId = useId();
  const copy = AUDIENCE_COPY[audience];

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

  function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    if (!EMAIL_RE.test(email.trim())) {
      setStatus("error");
      return;
    }
    setStatus("success");
  }

  const swap = reduce ? 0 : 0.35;

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-4 py-20 sm:py-28 dark:bg-slate-950">
      {/* ambient background shapes */}
      <div
        aria-hidden="true"
        className="ctaxcard-blob pointer-events-none absolute -left-24 top-10 h-72 w-72 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-600/20"
      />
      <div
        aria-hidden="true"
        className="ctaxcard-blob pointer-events-none absolute -right-24 bottom-0 h-80 w-80 rounded-full bg-violet-300/30 blur-3xl dark:bg-violet-700/20"
        style={{ animationDelay: "1.4s" }}
      />

      <motion.div
        initial={reduce ? false : { opacity: 0, y: 28 }}
        whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true, amount: 0.3 }}
        transition={{ duration: reduce ? 0 : 0.6, ease: "easeOut" }}
        className="relative mx-auto max-w-4xl"
      >
        {/* rotating color halo */}
        <div
          aria-hidden="true"
          className="ctaxcard-glow absolute -inset-6 rounded-[3rem] opacity-25 blur-2xl dark:opacity-40"
          style={{
            background:
              "conic-gradient(from 0deg, #6366f1, #a855f7, #38bdf8, #6366f1)",
          }}
        />

        <div className="relative overflow-hidden rounded-[2rem] bg-gradient-to-b from-white to-slate-50 p-8 shadow-2xl ring-1 ring-slate-200 sm:p-12 dark:from-slate-900 dark:to-slate-950 dark:ring-slate-800">
          {/* dot grid texture */}
          <div
            aria-hidden="true"
            className="pointer-events-none absolute inset-0 opacity-[0.35] dark:opacity-20"
            style={{
              backgroundImage:
                "radial-gradient(currentColor 1px, transparent 1px)",
              backgroundSize: "22px 22px",
              color: "#94a3b8",
              maskImage:
                "linear-gradient(to bottom, black, transparent 65%)",
              WebkitMaskImage:
                "linear-gradient(to bottom, black, transparent 65%)",
            }}
          />

          <div className="relative">
            {/* top row: badge + audience switch */}
            <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
              <span className="inline-flex w-fit items-center gap-2 rounded-full bg-indigo-50 px-3 py-1 text-xs font-semibold tracking-wide text-indigo-700 ring-1 ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/20">
                <span className="relative flex h-2 w-2">
                  <span className="ctaxcard-dot absolute inline-flex h-full w-full rounded-full bg-indigo-500 opacity-75" />
                  <span className="relative inline-flex h-2 w-2 rounded-full bg-indigo-500" />
                </span>
                NOW IN OPEN BETA
              </span>

              <div
                role="group"
                aria-label="Choose what to see"
                className="inline-flex self-start rounded-full bg-slate-100 p-1 text-sm font-medium ring-1 ring-slate-200 sm:self-auto dark:bg-slate-800/80 dark:ring-slate-700"
              >
                {(["teams", "solo"] as const).map((key) => {
                  const active = audience === key;
                  return (
                    <button
                      key={key}
                      type="button"
                      aria-pressed={active}
                      onClick={() => setAudience(key)}
                      className={
                        "rounded-full px-4 py-1.5 transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 motion-reduce:transition-none dark:focus-visible:ring-offset-slate-800 " +
                        (active
                          ? "bg-white text-slate-900 shadow-sm dark:bg-slate-950 dark:text-white"
                          : "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200")
                      }
                    >
                      {key === "teams" ? "For teams" : "For solo work"}
                    </button>
                  );
                })}
              </div>
            </div>

            {/* swappable copy block */}
            <AnimatePresence mode="wait">
              <motion.div
                key={audience}
                initial={reduce ? false : { opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                exit={reduce ? { opacity: 1 } : { opacity: 0, y: -10 }}
                transition={{ duration: swap, ease: "easeOut" }}
                className="mt-7 max-w-2xl"
              >
                <h2 className="text-3xl font-bold leading-tight tracking-tight text-slate-900 sm:text-4xl dark:text-white">
                  {copy.headline}
                </h2>
                <p className="mt-4 text-base leading-relaxed text-slate-600 sm:text-lg dark:text-slate-300">
                  {copy.sub}
                </p>
                <p className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-emerald-700 dark:text-emerald-400">
                  <svg
                    viewBox="0 0 20 20"
                    fill="none"
                    aria-hidden="true"
                    className="h-5 w-5 shrink-0"
                  >
                    <circle cx="10" cy="10" r="9" className="fill-emerald-100 dark:fill-emerald-500/15" />
                    <path
                      d="M6 10.5l2.5 2.5L14 7.5"
                      stroke="currentColor"
                      strokeWidth="1.8"
                      strokeLinecap="round"
                      strokeLinejoin="round"
                    />
                  </svg>
                  {copy.bullet}
                </p>
              </motion.div>
            </AnimatePresence>

            {/* form / success swap */}
            <div className="mt-8">
              <AnimatePresence mode="wait" initial={false}>
                {status === "success" ? (
                  <motion.div
                    key="success"
                    initial={reduce ? false : { opacity: 0, y: 8 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={reduce ? { opacity: 1 } : { opacity: 0, y: -8 }}
                    transition={{ duration: swap, ease: "easeOut" }}
                    role="status"
                    aria-live="polite"
                    className="flex items-start gap-3 rounded-2xl bg-emerald-50 p-5 ring-1 ring-emerald-200 dark:bg-emerald-500/10 dark:ring-emerald-400/20"
                  >
                    <svg
                      viewBox="0 0 24 24"
                      fill="none"
                      aria-hidden="true"
                      className="mt-0.5 h-6 w-6 shrink-0 text-emerald-600 dark:text-emerald-400"
                    >
                      <path
                        d="M20 6L9 17l-5-5"
                        stroke="currentColor"
                        strokeWidth="2.2"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                      />
                    </svg>
                    <div>
                      <p className="font-semibold text-emerald-900 dark:text-emerald-200">
                        You're on the list.
                      </p>
                      <p className="mt-1 text-sm text-emerald-800/80 dark:text-emerald-300/80">
                        We sent a confirmation to{" "}
                        <span className="font-medium">{email.trim()}</span>. Tap
                        the link and your workspace will be ready.
                      </p>
                    </div>
                  </motion.div>
                ) : (
                  <motion.form
                    key="form"
                    initial={reduce ? false : { opacity: 0, y: 8 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={reduce ? { opacity: 1 } : { opacity: 0, y: -8 }}
                    transition={{ duration: swap, ease: "easeOut" }}
                    onSubmit={handleSubmit}
                    noValidate
                    className="flex flex-col gap-3"
                  >
                    <div className="flex flex-col gap-3 sm:flex-row">
                      <div className="flex-1">
                        <label htmlFor={emailId} className="sr-only">
                          Work email
                        </label>
                        <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 : undefined
                          }
                          className={
                            "w-full rounded-xl border bg-white px-4 py-3 text-base text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-slate-900 dark:text-white dark:placeholder:text-slate-500 " +
                            (status === "error"
                              ? "border-rose-400 focus:ring-rose-500 dark:border-rose-500"
                              : "border-slate-300 dark:border-slate-700")
                          }
                        />
                      </div>

                      <button
                        type="submit"
                        className="group relative inline-flex items-center justify-center gap-2 overflow-hidden rounded-xl bg-gradient-to-br from-indigo-600 to-violet-600 px-6 py-3 text-base font-semibold text-white shadow-lg shadow-indigo-600/20 transition-transform duration-200 hover:-translate-y-0.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white motion-reduce:transition-none motion-reduce:hover:translate-y-0 dark:focus-visible:ring-offset-slate-900"
                      >
                        <span
                          aria-hidden="true"
                          className="absolute inset-0 -translate-x-full -skew-x-12 bg-white/25 transition-transform duration-700 group-hover:translate-x-full motion-reduce:hidden"
                        />
                        <span className="relative">Start free</span>
                        <svg
                          viewBox="0 0 20 20"
                          fill="none"
                          aria-hidden="true"
                          className="relative h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5 motion-reduce:transition-none"
                        >
                          <path
                            d="M4 10h11M11 5l5 5-5 5"
                            stroke="currentColor"
                            strokeWidth="1.8"
                            strokeLinecap="round"
                            strokeLinejoin="round"
                          />
                        </svg>
                      </button>
                    </div>

                    {status === "error" ? (
                      <p
                        id={errorId}
                        role="alert"
                        className="text-sm font-medium text-rose-600 dark:text-rose-400"
                      >
                        Please enter a valid email address so we can send your
                        invite.
                      </p>
                    ) : (
                      <p className="text-sm text-slate-500 dark:text-slate-400">
                        Free 14-day trial · No credit card · Cancel anytime.
                      </p>
                    )}
                  </motion.form>
                )}
              </AnimatePresence>
            </div>

            {/* divider */}
            <div className="mt-8 h-px w-full bg-slate-200 dark:bg-slate-800" />

            {/* trust row */}
            <div className="mt-6 flex flex-col gap-5 sm:flex-row sm:items-center sm:justify-between">
              <div className="flex items-center gap-3">
                <div className="flex -space-x-2">
                  {AVATARS.map((a) => (
                    <span
                      key={a.initials}
                      className={
                        "flex h-9 w-9 items-center justify-center rounded-full bg-gradient-to-br text-xs font-semibold text-white ring-2 ring-white dark:ring-slate-900 " +
                        a.from +
                        " " +
                        a.to
                      }
                    >
                      {a.initials}
                    </span>
                  ))}
                </div>
                <p className="text-sm text-slate-600 dark:text-slate-400">
                  Trusted by{" "}
                  <span className="font-semibold text-slate-900 dark:text-white">
                    12,000+
                  </span>{" "}
                  teams
                </p>
              </div>

              <div className="flex items-center gap-2">
                <div className="flex" aria-hidden="true">
                  {[0, 1, 2, 3, 4].map((i) => (
                    <svg
                      key={i}
                      viewBox="0 0 20 20"
                      className="h-4 w-4 fill-amber-400"
                    >
                      <path d="M10 1.5l2.6 5.3 5.8.8-4.2 4.1 1 5.8L10 14.9 4.8 17.5l1-5.8L1.6 7.6l5.8-.8z" />
                    </svg>
                  ))}
                </div>
                <p className="text-sm text-slate-600 dark:text-slate-400">
                  <span className="font-semibold text-slate-900 dark:text-white">
                    4.9
                  </span>{" "}
                  from 900+ reviews
                </p>
              </div>
            </div>
          </div>
        </div>
      </motion.div>

      <style>{`
        @keyframes ctaxcard-spin {
          to { transform: rotate(360deg); }
        }
        @keyframes ctaxcard-dotpulse {
          0%, 100% { opacity: .75; transform: scale(1); }
          50% { opacity: 0; transform: scale(2.2); }
        }
        @keyframes ctaxcard-float {
          0%, 100% { transform: translateY(0); }
          50% { transform: translateY(-18px); }
        }
        .ctaxcard-glow { animation: ctaxcard-spin 9s linear infinite; }
        .ctaxcard-dot { animation: ctaxcard-dotpulse 2s ease-in-out infinite; }
        .ctaxcard-blob { animation: ctaxcard-float 7s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .ctaxcard-glow,
          .ctaxcard-dot,
          .ctaxcard-blob {
            animation: none !important;
          }
        }
      `}</style>
    </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 →