Image Side CTA
Original · freeCTA with a product image
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"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 “what’s working?” 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’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
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 quoteSimilar components
Browse all →
Gradient CTA
OriginalA bold gradient call-to-action banner with two buttons.

Full-Bleed Dark CTA
OriginalA full-bleed dark call-to-action banner with a live status badge, subtle grid backdrop and a primary plus secondary button pair to drive discovery calls.

Split CTA With Lead Form
OriginalA two-column call-to-action that pairs a benefit checklist with an inline name, email and message form for capturing qualified leads directly in the section.

Newsletter CTA With Stats
OriginalA newsletter sign-up call-to-action combining an inline email subscribe form with a three-cell social-proof stat strip to boost conversions.

Aurora Glow CTA Banner
OriginalA full-bleed dark call-to-action banner with a rotating aurora gradient, floating orbs and a shine-on-hover button, with staggered blur-in text that reveals on scroll.

Animated Border Beam CTA
OriginalAn email-capture call-to-action card wrapped in a rotating conic-gradient border beam, with staggered reveal, animated shine text and an inline success state on submit.

Marquee Ribbon CTA
OriginalA dark call-to-action band framed by two infinite scrolling tag ribbons with a continuously flowing animated-gradient button and staggered centre content.

Centered CTA
Originalcentred CTA band

Split CTA
Originalsplit CTA with image side

Gradient Band CTA
Originalgradient CTA band

Newsletter CTA
OriginalCTA with newsletter input

App Download CTA
Originalapp download CTA with store buttons

