Web InnoventixFreeCode

Split CTA

Original · free

split CTA with image side

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

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

/* ---------------------------------------------------------------- icons */

function IconArrow({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={className}>
      <path
        d="M4 10h11m0 0-4.5-4.5M15 10l-4.5 4.5"
        stroke="currentColor"
        strokeWidth="1.6"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function IconCheck({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={className}>
      <path
        d="m4.5 10.5 3.5 3.5 7.5-8"
        stroke="currentColor"
        strokeWidth="1.8"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function IconStar({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" className={className}>
      <path d="M10 1.6l2.47 5.01 5.53.8-4 3.9.94 5.5L10 14.2l-4.94 2.6.94-5.5-4-3.9 5.53-.8L10 1.6z" />
    </svg>
  );
}

function IconBolt({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={className}>
      <path
        d="M11 2 4 11h5l-1 7 7-9h-5l1-7z"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function IconSpark({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={className}>
      <path
        d="M10 2.5v4M10 13.5v4M2.5 10h4M13.5 10h4M5 5l2.5 2.5M12.5 12.5 15 15M15 5l-2.5 2.5M7.5 12.5 5 15"
        stroke="currentColor"
        strokeWidth="1.4"
        strokeLinecap="round"
      />
    </svg>
  );
}

/* ------------------------------------------------------------- animation */

const container: Variants = {
  hidden: {},
  show: { transition: { staggerChildren: 0.08, delayChildren: 0.04 } },
};

const rise: Variants = {
  hidden: { opacity: 0, y: 18 },
  show: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.55, ease: [0.22, 1, 0.36, 1] },
  },
};

const still: Variants = {
  hidden: { opacity: 1, y: 0 },
  show: { opacity: 1, y: 0 },
};

/* ----------------------------------------------------------------- data */

const bars: number[] = [38, 61, 49, 82, 70, 94, 58];
const perks: string[] = ["No credit card", "14-day full access", "Cancel anytime"];

/* ------------------------------------------------------------ component */

export default function CtaxSplit() {
  const reduce = useReducedMotion();
  const variant = reduce ? still : rise;

  const [email, setEmail] = useState<string>("");
  const [status, setStatus] = useState<"idle" | "invalid" | "success">("idle");

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

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

  function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    if (!isValid(email)) {
      setStatus("invalid");
      return;
    }
    setStatus("success");
  }

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-4 py-20 sm:py-28 dark:bg-slate-950">
      <style>{`
        @keyframes ctaxsplit-float {
          0%, 100% { transform: translateY(0); }
          50% { transform: translateY(-12px); }
        }
        @keyframes ctaxsplit-floatslow {
          0%, 100% { transform: translateY(0) translateX(0); }
          50% { transform: translateY(10px) translateX(-6px); }
        }
        @keyframes ctaxsplit-drift {
          0%, 100% { transform: translate(0, 0) scale(1); opacity: .8; }
          50% { transform: translate(18px, -14px) scale(1.08); opacity: 1; }
        }
        @keyframes ctaxsplit-sheen {
          0% { transform: translateX(-120%); }
          60%, 100% { transform: translateX(220%); }
        }
        @keyframes ctaxsplit-rise {
          0% { transform: scaleY(.55); opacity: .55; }
          100% { transform: scaleY(1); opacity: 1; }
        }
        @media (prefers-reduced-motion: reduce) {
          .ctaxsplit-anim { animation: none !important; }
        }
      `}</style>

      {/* ambient background wash */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0">
        <div className="ctaxsplit-anim absolute -top-24 left-1/4 h-72 w-72 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-600/20 [animation:ctaxsplit-drift_14s_ease-in-out_infinite]" />
        <div className="ctaxsplit-anim absolute -bottom-28 right-1/5 h-80 w-80 rounded-full bg-violet-300/30 blur-3xl dark:bg-violet-700/20 [animation:ctaxsplit-drift_18s_ease-in-out_infinite_reverse]" />
      </div>

      <motion.div
        variants={container}
        initial="hidden"
        whileInView="show"
        viewport={{ once: true, amount: 0.3 }}
        className="relative mx-auto grid max-w-6xl overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-xl shadow-slate-900/5 lg:grid-cols-2 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/30"
      >
        {/* ------------------------------------------------ copy / form side */}
        <div className="flex flex-col justify-center gap-7 p-8 sm:p-12 lg:p-14">
          <motion.span
            variants={variant}
            className="inline-flex w-fit items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3.5 py-1.5 text-xs font-semibold tracking-wide text-indigo-700 uppercase dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300"
          >
            <IconSpark className="h-3.5 w-3.5" />
            Now in early access
          </motion.span>

          <motion.h2
            variants={variant}
            className="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-[2.6rem] lg:leading-[1.1] dark:text-white"
          >
            Ship your next release{" "}
            <span className="bg-gradient-to-r from-indigo-600 via-violet-600 to-indigo-500 bg-clip-text text-transparent dark:from-indigo-400 dark:via-violet-400 dark:to-indigo-300">
              without the busywork
            </span>
          </motion.h2>

          <motion.p
            variants={variant}
            className="max-w-md text-base leading-relaxed text-slate-600 sm:text-lg dark:text-slate-300"
          >
            Northline turns scattered specs, tickets, and reviews into one
            planning workspace — so your team spends less time coordinating and
            more time shipping.
          </motion.p>

          {/* email capture */}
          <motion.div variants={variant} className="w-full max-w-md">
            {status === "success" ? (
              <div
                role="status"
                aria-live="polite"
                className="flex items-start gap-3 rounded-2xl border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-500/30 dark:bg-emerald-500/10"
              >
                <span className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white">
                  <IconCheck className="h-4 w-4" />
                </span>
                <div className="text-sm">
                  <p className="font-semibold text-emerald-800 dark:text-emerald-200">
                    You&apos;re on the list.
                  </p>
                  <p className="mt-0.5 text-emerald-700/90 dark:text-emerald-300/80">
                    We sent a setup link to{" "}
                    <span className="font-medium">{email.trim()}</span>.
                  </p>
                  <button
                    type="button"
                    onClick={() => {
                      setEmail("");
                      setStatus("idle");
                    }}
                    className="mt-2 rounded-sm text-xs font-semibold text-emerald-700 underline underline-offset-2 hover:text-emerald-900 focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:outline-none dark:text-emerald-300 dark:hover:text-emerald-100"
                  >
                    Use a different email
                  </button>
                </div>
              </div>
            ) : (
              <form onSubmit={handleSubmit} noValidate className="flex flex-col gap-2">
                <div className="flex flex-col gap-2 sm:flex-row">
                  <div className="flex-1">
                    <label htmlFor="ctaxsplit-email" className="sr-only">
                      Work email
                    </label>
                    <input
                      id="ctaxsplit-email"
                      type="email"
                      inputMode="email"
                      autoComplete="email"
                      value={email}
                      onChange={handleChange}
                      placeholder="you@company.com"
                      aria-invalid={status === "invalid"}
                      aria-describedby={
                        status === "invalid" ? "ctaxsplit-error" : undefined
                      }
                      className={`w-full rounded-xl border bg-white px-4 py-3 text-sm text-slate-900 placeholder-slate-400 transition focus-visible:ring-2 focus-visible:outline-none dark:bg-slate-800 dark:text-white dark:placeholder-slate-500 ${
                        status === "invalid"
                          ? "border-rose-400 focus-visible:ring-rose-500 dark:border-rose-500/60"
                          : "border-slate-300 focus-visible:ring-indigo-500 dark:border-slate-700"
                      }`}
                    />
                  </div>
                  <button
                    type="submit"
                    className="group inline-flex items-center justify-center gap-2 rounded-xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-sm shadow-indigo-600/20 transition hover:bg-indigo-500 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:outline-none active:translate-y-px dark:focus-visible:ring-offset-slate-900"
                  >
                    Start free
                    <IconArrow className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
                  </button>
                </div>
                {status === "invalid" && (
                  <p
                    id="ctaxsplit-error"
                    role="alert"
                    className="text-xs font-medium text-rose-600 dark:text-rose-400"
                  >
                    Please enter a valid email address.
                  </p>
                )}
              </form>
            )}

            <ul className="mt-4 flex flex-wrap gap-x-5 gap-y-2">
              {perks.map((perk) => (
                <li
                  key={perk}
                  className="flex items-center gap-1.5 text-xs font-medium text-slate-500 dark:text-slate-400"
                >
                  <IconCheck className="h-3.5 w-3.5 text-emerald-500" />
                  {perk}
                </li>
              ))}
            </ul>
          </motion.div>

          {/* social proof */}
          <motion.div
            variants={variant}
            className="flex items-center gap-3 border-t border-slate-200 pt-6 dark:border-slate-800"
          >
            <div className="flex -space-x-2" aria-hidden="true">
              {["from-indigo-400 to-violet-500", "from-sky-400 to-indigo-500", "from-violet-400 to-rose-400", "from-emerald-400 to-sky-500"].map(
                (g, i) => (
                  <span
                    key={i}
                    className={`h-8 w-8 rounded-full bg-gradient-to-br ${g} ring-2 ring-white dark:ring-slate-900`}
                  />
                ),
              )}
            </div>
            <div className="text-xs leading-tight text-slate-600 dark:text-slate-400">
              <span className="flex items-center gap-1 text-amber-500">
                {Array.from({ length: 5 }).map((_, i) => (
                  <IconStar key={i} className="h-3.5 w-3.5" />
                ))}
                <span className="ml-1 font-semibold text-slate-800 dark:text-slate-200">
                  4.9/5
                </span>
              </span>
              <span>Loved by 4,000+ product teams</span>
            </div>
          </motion.div>
        </div>

        {/* --------------------------------------------------- visual side */}
        <div className="relative min-h-[22rem] overflow-hidden bg-gradient-to-br from-indigo-600 via-violet-600 to-indigo-700 p-8 sm:p-12 lg:p-14 dark:from-indigo-700 dark:via-violet-800 dark:to-slate-900">
          {/* grid + glow decor */}
          <div aria-hidden="true" className="pointer-events-none absolute inset-0">
            <div
              className="absolute inset-0 opacity-[0.18]"
              style={{
                backgroundImage:
                  "linear-gradient(rgba(255,255,255,.6) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.6) 1px, transparent 1px)",
                backgroundSize: "34px 34px",
                maskImage:
                  "radial-gradient(120% 90% at 70% 15%, #000 40%, transparent 80%)",
                WebkitMaskImage:
                  "radial-gradient(120% 90% at 70% 15%, #000 40%, transparent 80%)",
              }}
            />
            <div className="ctaxsplit-anim absolute -right-10 -top-10 h-56 w-56 rounded-full bg-white/20 blur-3xl [animation:ctaxsplit-drift_16s_ease-in-out_infinite]" />
          </div>

          {/* dashboard glass card */}
          <div className="relative flex h-full items-center justify-center">
            <div className="ctaxsplit-anim relative w-full max-w-sm rounded-2xl border border-white/25 bg-white/12 p-5 shadow-2xl shadow-black/20 backdrop-blur-md [animation:ctaxsplit-float_7s_ease-in-out_infinite]">
              {/* sheen sweep */}
              <div
                aria-hidden="true"
                className="pointer-events-none absolute inset-0 overflow-hidden rounded-2xl"
              >
                <div className="ctaxsplit-anim absolute inset-y-0 -left-1/3 w-1/3 skew-x-12 bg-gradient-to-r from-transparent via-white/25 to-transparent [animation:ctaxsplit-sheen_6s_ease-in-out_infinite]" />
              </div>

              <div className="relative">
                <div className="flex items-center justify-between">
                  <div className="flex items-center gap-2">
                    <span className="flex h-8 w-8 items-center justify-center rounded-lg bg-white/25 text-white">
                      <IconBolt className="h-4 w-4" />
                    </span>
                    <div className="leading-tight">
                      <p className="text-sm font-semibold text-white">Sprint 24</p>
                      <p className="text-[11px] text-white/70">On track</p>
                    </div>
                  </div>
                  <span className="inline-flex items-center gap-1 rounded-full bg-emerald-400/90 px-2 py-0.5 text-[11px] font-semibold text-emerald-950">
                    <span className="h-1.5 w-1.5 rounded-full bg-emerald-950/70" />
                    Live
                  </span>
                </div>

                <div className="mt-5">
                  <p className="text-xs text-white/70">Cycle time this month</p>
                  <p className="mt-1 flex items-end gap-2">
                    <span className="text-3xl font-bold tracking-tight text-white">
                      2.4d
                    </span>
                    <span className="mb-1 text-xs font-semibold text-emerald-300">
                      -38%
                    </span>
                  </p>
                </div>

                {/* bar chart */}
                <div
                  className="mt-4 flex h-24 items-end gap-2"
                  aria-hidden="true"
                >
                  {bars.map((h, i) => (
                    <div
                      key={i}
                      className="ctaxsplit-anim flex-1 origin-bottom rounded-t-md bg-gradient-to-t from-white/40 to-white/85"
                      style={{
                        height: `${h}%`,
                        animation: "ctaxsplit-rise .8s ease-out both",
                        animationDelay: `${i * 90}ms`,
                      }}
                    />
                  ))}
                </div>

                <div className="mt-5 space-y-2.5">
                  {[
                    { label: "Auth service", value: "Merged" },
                    { label: "Billing refactor", value: "In review" },
                  ].map((row) => (
                    <div
                      key={row.label}
                      className="flex items-center justify-between rounded-lg bg-white/10 px-3 py-2"
                    >
                      <span className="flex items-center gap-2 text-xs font-medium text-white">
                        <span className="h-1.5 w-1.5 rounded-full bg-violet-200" />
                        {row.label}
                      </span>
                      <span className="text-[11px] text-white/70">
                        {row.value}
                      </span>
                    </div>
                  ))}
                </div>
              </div>
            </div>

            {/* floating badge */}
            <div className="ctaxsplit-anim absolute bottom-6 left-4 hidden items-center gap-2 rounded-xl border border-white/25 bg-white/15 px-3 py-2 shadow-lg backdrop-blur-md sm:flex [animation:ctaxsplit-floatslow_9s_ease-in-out_infinite]">
              <span className="flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-400 text-emerald-950">
                <IconCheck className="h-4 w-4" />
              </span>
              <div className="leading-tight">
                <p className="text-xs font-semibold text-white">Deploy ready</p>
                <p className="text-[10px] text-white/70">All checks passed</p>
              </div>
            </div>
          </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 →