Web InnoventixFreeCode

Image Side CTA

Original · free

CTA with a product image

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

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

export default function CtaxImageSide() {
  const reduce = useReducedMotion();
  const uid = useId();
  const emailId = `${uid}-email`;
  const statusId = `${uid}-status`;
  const areaGrad = `${uid}-area`;
  const lineGrad = `${uid}-line`;

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

  function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
    setStatus(valid ? "success" : "error");
  }

  const container: Variants = {
    hidden: {},
    visible: {
      transition: { staggerChildren: reduce ? 0 : 0.09, delayChildren: 0.04 },
    },
  };

  const item: Variants = {
    hidden: { opacity: 0, y: 22 },
    visible: {
      opacity: 1,
      y: 0,
      transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] },
    },
  };

  const panel: Variants = {
    hidden: { opacity: 0, y: 28, scale: 0.98 },
    visible: {
      opacity: 1,
      y: 0,
      scale: 1,
      transition: { duration: 0.75, ease: [0.22, 1, 0.36, 1] },
    },
  };

  const kpis: ReadonlyArray<{ label: string; value: string; delta: string }> = [
    { label: "Active users", value: "24,918", delta: "+8.2%" },
    { label: "Retention", value: "91.4%", delta: "+3.1%" },
    { label: "Avg. session", value: "6m 12s", delta: "+0.9%" },
  ];

  return (
    <section className="relative w-full overflow-hidden bg-white py-20 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:py-28 lg:py-32">
      <style>{`
        @keyframes ctaxis-floatA { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-12px)} }
        @keyframes ctaxis-floatB { 0%,100%{transform:translateY(0)} 50%{transform:translateY(9px)} }
        @keyframes ctaxis-draw { from{stroke-dashoffset:640} to{stroke-dashoffset:0} }
        @keyframes ctaxis-pulse { 0%,100%{opacity:.9;transform:scale(1)} 50%{opacity:.35;transform:scale(1.8)} }
        @keyframes ctaxis-rise { from{transform:scaleY(.35);opacity:.55} to{transform:scaleY(1);opacity:1} }
        .ctaxis-floatA{animation:ctaxis-floatA 6.5s ease-in-out infinite}
        .ctaxis-floatB{animation:ctaxis-floatB 7.5s ease-in-out infinite}
        .ctaxis-draw{stroke-dasharray:640;stroke-dashoffset:0;animation:ctaxis-draw 2.1s cubic-bezier(.22,1,.36,1) forwards}
        .ctaxis-pulse{transform-origin:center;animation:ctaxis-pulse 2.4s ease-in-out infinite}
        .ctaxis-bar{transform-origin:bottom;animation:ctaxis-rise .9s ease-out both}
        @media (prefers-reduced-motion: reduce){
          .ctaxis-floatA,.ctaxis-floatB,.ctaxis-draw,.ctaxis-pulse,.ctaxis-bar{animation:none!important}
          .ctaxis-draw{stroke-dashoffset:0!important}
          .ctaxis-bar{transform:none!important;opacity:1!important}
        }
      `}</style>

      {/* decorative background */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10">
        <div className="absolute -left-24 top-8 h-72 w-72 rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-500/10" />
        <div className="absolute -right-16 bottom-0 h-80 w-80 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-600/10" />
        <div
          className="absolute inset-0 opacity-[0.5] dark:opacity-[0.35]"
          style={{
            backgroundImage:
              "radial-gradient(circle at 1px 1px, rgba(100,116,139,0.16) 1px, transparent 0)",
            backgroundSize: "28px 28px",
            maskImage:
              "radial-gradient(ellipse 80% 60% at 50% 40%, black 40%, transparent 100%)",
            WebkitMaskImage:
              "radial-gradient(ellipse 80% 60% at 50% 40%, black 40%, transparent 100%)",
          }}
        />
      </div>

      <motion.div
        variants={container}
        initial={reduce ? "visible" : "hidden"}
        whileInView="visible"
        viewport={{ once: true, margin: "-80px" }}
        className="mx-auto grid max-w-6xl grid-cols-1 items-center gap-14 px-6 lg:grid-cols-2 lg:gap-16"
      >
        {/* left: copy + CTA */}
        <div className="max-w-xl">
          <motion.span
            variants={item}
            className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-indigo-700 dark:border-indigo-900/60 dark:bg-indigo-950/50 dark:text-indigo-300"
          >
            <span className="relative flex h-1.5 w-1.5">
              <span className="absolute inline-flex h-full w-full rounded-full bg-emerald-500 opacity-75 ctaxis-pulse" />
              <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
            </span>
            Now with live cohorts
          </motion.span>

          <motion.h2
            variants={item}
            className="mt-5 text-4xl font-bold tracking-tight sm:text-5xl"
          >
            Turn scattered product data into decisions your team{" "}
            <span className="bg-gradient-to-r from-indigo-600 to-violet-600 bg-clip-text text-transparent dark:from-indigo-400 dark:to-violet-400">
              actually trusts
            </span>
            .
          </motion.h2>

          <motion.p
            variants={item}
            className="mt-5 text-lg leading-relaxed text-slate-600 dark:text-slate-300"
          >
            Beacon unifies every event, funnel, and cohort into one live workspace,
            so you can answer &ldquo;what&rsquo;s working?&rdquo; in seconds, not sprints.
            No SQL, no data engineer, no waiting on the weekly export.
          </motion.p>

          <motion.ul variants={item} className="mt-7 space-y-3">
            {[
              "Live dashboards up and running in under 5 minutes",
              "Self-serve funnels and retention, no query language",
              "SOC 2 Type II and GDPR-ready from day one",
            ].map((line) => (
              <li key={line} className="flex items-start gap-3">
                <span className="mt-0.5 flex h-5 w-5 flex-none items-center justify-center rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-400">
                  <svg viewBox="0 0 20 20" fill="currentColor" className="h-3.5 w-3.5" aria-hidden="true">
                    <path
                      fillRule="evenodd"
                      d="M16.7 5.3a1 1 0 0 1 0 1.4l-7.5 7.5a1 1 0 0 1-1.4 0l-3.5-3.5a1 1 0 1 1 1.4-1.4l2.8 2.8 6.8-6.8a1 1 0 0 1 1.4 0Z"
                      clipRule="evenodd"
                    />
                  </svg>
                </span>
                <span className="text-[0.95rem] text-slate-700 dark:text-slate-300">{line}</span>
              </li>
            ))}
          </motion.ul>

          <motion.div variants={item} className="mt-8">
            {status === "success" ? (
              <div
                role="status"
                className="flex items-start gap-3 rounded-2xl border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-900/60 dark:bg-emerald-950/40"
              >
                <span className="mt-0.5 flex h-6 w-6 flex-none items-center justify-center rounded-full bg-emerald-600 text-white">
                  <svg viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4" aria-hidden="true">
                    <path
                      fillRule="evenodd"
                      d="M16.7 5.3a1 1 0 0 1 0 1.4l-7.5 7.5a1 1 0 0 1-1.4 0l-3.5-3.5a1 1 0 1 1 1.4-1.4l2.8 2.8 6.8-6.8a1 1 0 0 1 1.4 0Z"
                      clipRule="evenodd"
                    />
                  </svg>
                </span>
                <div>
                  <p className="font-semibold text-emerald-900 dark:text-emerald-200">
                    You&rsquo;re on the list.
                  </p>
                  <p className="text-sm text-emerald-800/80 dark:text-emerald-300/80">
                    Check <span className="font-medium">{email.trim()}</span> for your workspace invite.
                  </p>
                </div>
              </div>
            ) : (
              <form 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 address
                    </label>
                    <input
                      id={emailId}
                      type="email"
                      inputMode="email"
                      autoComplete="email"
                      placeholder="you@company.com"
                      value={email}
                      onChange={(e) => {
                        setEmail(e.target.value);
                        if (status === "error") setStatus("idle");
                      }}
                      aria-invalid={status === "error"}
                      aria-describedby={status === "error" ? statusId : undefined}
                      className={`w-full rounded-xl border bg-white px-4 py-3 text-slate-900 shadow-sm outline-none transition placeholder:text-slate-400 focus:ring-4 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-500 ${
                        status === "error"
                          ? "border-rose-400 focus:border-rose-500 focus:ring-rose-500/20 dark:border-rose-500/70"
                          : "border-slate-300 focus:border-indigo-500 focus:ring-indigo-500/20 dark:border-slate-700"
                      }`}
                    />
                  </div>
                  <button
                    type="submit"
                    className="group inline-flex items-center justify-center gap-2 rounded-xl bg-indigo-600 px-6 py-3 font-semibold text-white shadow-lg shadow-indigo-600/25 transition hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-indigo-500/40 active:scale-[0.98] dark:shadow-indigo-950/40"
                  >
                    Start free trial
                    <svg
                      viewBox="0 0 20 20"
                      fill="none"
                      className="h-4 w-4 transition-transform group-hover:translate-x-0.5"
                      aria-hidden="true"
                    >
                      <path
                        d="M4 10h11m0 0-4-4m4 4-4 4"
                        stroke="currentColor"
                        strokeWidth="2"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                      />
                    </svg>
                  </button>
                </div>
                <p id={statusId} aria-live="polite" className="min-h-[1.25rem] text-sm">
                  {status === "error" ? (
                    <span className="text-rose-600 dark:text-rose-400">
                      Please enter a valid work email address.
                    </span>
                  ) : (
                    <span className="text-slate-500 dark:text-slate-400">
                      14-day trial, no credit card. Cancel anytime.
                    </span>
                  )}
                </p>
              </form>
            )}
          </motion.div>

          <motion.div
            variants={item}
            className="mt-7 flex flex-wrap items-center gap-x-5 gap-y-3 text-sm text-slate-500 dark:text-slate-400"
          >
            <div className="flex items-center gap-1.5">
              <div className="flex" aria-hidden="true">
                {[0, 1, 2, 3, 4].map((i) => (
                  <svg key={i} viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4 text-amber-400">
                    <path d="M10 1.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8L10 15l-5.2 2.6 1-5.8L1.5 7.7l5.9-.9L10 1.5Z" />
                  </svg>
                ))}
              </div>
              <span className="font-semibold text-slate-700 dark:text-slate-200">4.9/5</span>
            </div>
            <span className="hidden h-4 w-px bg-slate-300 dark:bg-slate-700 sm:block" />
            <span>Trusted by 4,000+ product teams</span>
          </motion.div>
        </div>

        {/* right: product mockup */}
        <motion.div variants={panel} className="relative">
          <div className="relative rounded-3xl border border-slate-200 bg-white/80 p-3 shadow-2xl shadow-slate-900/10 backdrop-blur dark:border-slate-800 dark:bg-slate-900/70 dark:shadow-black/40">
            {/* window chrome */}
            <div className="flex items-center gap-2 px-3 pb-3 pt-1.5">
              <span className="h-3 w-3 rounded-full bg-rose-400" />
              <span className="h-3 w-3 rounded-full bg-amber-400" />
              <span className="h-3 w-3 rounded-full bg-emerald-400" />
              <div className="ml-3 flex-1">
                <div className="mx-auto flex h-6 max-w-[16rem] items-center justify-center rounded-md bg-slate-100 px-3 text-[0.7rem] font-medium text-slate-500 dark:bg-slate-800 dark:text-slate-400">
                  app.beacon.io/overview
                </div>
              </div>
            </div>

            {/* app body */}
            <div className="rounded-2xl border border-slate-200 bg-gradient-to-b from-slate-50 to-white p-5 dark:border-slate-800 dark:from-slate-900 dark:to-slate-950">
              <div className="flex items-center justify-between">
                <div>
                  <p className="text-xs font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
                    Weekly activity
                  </p>
                  <p className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white">
                    128.4k
                  </p>
                </div>
                <div className="flex rounded-lg bg-slate-100 p-0.5 text-xs font-medium dark:bg-slate-800">
                  <span className="rounded-md bg-white px-2.5 py-1 text-slate-900 shadow-sm dark:bg-slate-700 dark:text-white">
                    7d
                  </span>
                  <span className="px-2.5 py-1 text-slate-500 dark:text-slate-400">30d</span>
                  <span className="px-2.5 py-1 text-slate-500 dark:text-slate-400">90d</span>
                </div>
              </div>

              {/* chart */}
              <svg viewBox="0 0 400 160" className="mt-4 h-40 w-full" aria-hidden="true">
                <defs>
                  <linearGradient id={areaGrad} x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0%" stopColor="rgb(99 102 241)" stopOpacity="0.35" />
                    <stop offset="100%" stopColor="rgb(99 102 241)" stopOpacity="0" />
                  </linearGradient>
                  <linearGradient id={lineGrad} x1="0" y1="0" x2="1" y2="0">
                    <stop offset="0%" stopColor="rgb(99 102 241)" />
                    <stop offset="100%" stopColor="rgb(139 92 246)" />
                  </linearGradient>
                </defs>
                {[40, 80, 120].map((y) => (
                  <line
                    key={y}
                    x1="0"
                    y1={y}
                    x2="400"
                    y2={y}
                    stroke="currentColor"
                    strokeOpacity="0.08"
                    strokeWidth="1"
                    className="text-slate-500"
                  />
                ))}
                <path
                  d="M0,120 L40,112 L80,124 L120,90 L160,100 L200,68 L240,80 L280,52 L320,60 L360,34 L400,44 L400,160 L0,160 Z"
                  fill={`url(#${areaGrad})`}
                />
                <path
                  d="M0,120 L40,112 L80,124 L120,90 L160,100 L200,68 L240,80 L280,52 L320,60 L360,34 L400,44"
                  fill="none"
                  stroke={`url(#${lineGrad})`}
                  strokeWidth="2.5"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  className="ctaxis-draw"
                />
                <circle cx="360" cy="34" r="4" fill="rgb(139 92 246)" className="ctaxis-pulse" />
                <circle cx="360" cy="34" r="4" fill="rgb(139 92 246)" />
              </svg>

              {/* kpi tiles */}
              <div className="mt-4 grid grid-cols-3 gap-2.5">
                {kpis.map((k) => (
                  <div
                    key={k.label}
                    className="rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-800 dark:bg-slate-900"
                  >
                    <p className="truncate text-[0.65rem] font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
                      {k.label}
                    </p>
                    <p className="mt-1 text-sm font-bold text-slate-900 dark:text-white">{k.value}</p>
                    <p className="text-[0.7rem] font-semibold text-emerald-600 dark:text-emerald-400">
                      {k.delta}
                    </p>
                  </div>
                ))}
              </div>
            </div>
          </div>

          {/* floating card: conversion */}
          <div className="ctaxis-floatA absolute -left-4 top-10 hidden rounded-2xl border border-slate-200 bg-white p-3 shadow-xl shadow-slate-900/10 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/50 sm:block">
            <div className="flex items-center gap-3">
              <span className="flex h-9 w-9 items-center justify-center rounded-lg bg-emerald-100 text-emerald-600 dark:bg-emerald-950/60 dark:text-emerald-400">
                <svg viewBox="0 0 20 20" fill="none" className="h-5 w-5" aria-hidden="true">
                  <path
                    d="M4 13l4-4 3 3 5-6"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                </svg>
              </span>
              <div>
                <p className="text-[0.65rem] font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
                  Conversion
                </p>
                <p className="text-sm font-bold text-slate-900 dark:text-white">
                  +12.4% <span className="text-emerald-500">this week</span>
                </p>
              </div>
            </div>
          </div>

          {/* floating card: signups */}
          <div className="ctaxis-floatB absolute -bottom-5 right-2 hidden rounded-2xl border border-slate-200 bg-white p-3 shadow-xl shadow-slate-900/10 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/50 sm:block">
            <p className="text-[0.65rem] font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
              New signups
            </p>
            <div className="mt-1 flex items-end gap-2">
              <p className="text-lg font-bold leading-none text-slate-900 dark:text-white">1,208</p>
              <svg viewBox="0 0 48 20" className="h-5 w-14" aria-hidden="true">
                {[3, 7, 5, 10, 8, 13, 16].map((h, i) => (
                  <rect
                    key={i}
                    x={i * 7}
                    y={20 - h}
                    width="4.5"
                    height={h}
                    rx="1.2"
                    className="ctaxis-bar fill-indigo-500 dark:fill-indigo-400"
                    style={{ animationDelay: `${i * 0.08}s` }}
                  />
                ))}
              </svg>
            </div>
          </div>
        </motion.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 →