CTA Top Footer
Original · freefooter with a CTA on top
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/footx-cta-top.json"use client";
import { useId, useState, type FormEvent, type ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type FreqValue = "weekly" | "monthly";
type Status = "idle" | "error" | "success";
const FREQ_OPTIONS: { value: FreqValue; label: string }[] = [
{ value: "weekly", label: "Weekly" },
{ value: "monthly", label: "Monthly" },
];
const FOOTER_COLUMNS: {
key: string;
title: string;
links: { label: string; href: string }[];
}[] = [
{
key: "product",
title: "Product",
links: [
{ label: "Features", href: "#features" },
{ label: "Integrations", href: "#integrations" },
{ label: "Changelog", href: "#changelog" },
{ label: "Roadmap", href: "#roadmap" },
{ label: "Pricing", href: "#pricing" },
],
},
{
key: "resources",
title: "Resources",
links: [
{ label: "Documentation", href: "#docs" },
{ label: "API reference", href: "#api" },
{ label: "Guides", href: "#guides" },
{ label: "Community", href: "#community" },
{ label: "System status", href: "#status" },
],
},
{
key: "company",
title: "Company",
links: [
{ label: "About", href: "#about" },
{ label: "Careers", href: "#careers" },
{ label: "Blog", href: "#blog" },
{ label: "Customers", href: "#customers" },
{ label: "Contact", href: "#contact" },
],
},
{
key: "legal",
title: "Legal",
links: [
{ label: "Privacy", href: "#privacy" },
{ label: "Terms", href: "#terms" },
{ label: "Security", href: "#security" },
{ label: "Cookie settings", href: "#cookies" },
{ label: "DPA", href: "#dpa" },
],
},
];
const SOCIALS: { label: string; href: string; icon: ReactNode }[] = [
{
label: "Meridian on X",
href: "#x",
icon: (
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.657l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
),
},
{
label: "Meridian on GitHub",
href: "#github",
icon: (
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
),
},
{
label: "Meridian on LinkedIn",
href: "#linkedin",
icon: (
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z" />
),
},
{
label: "Meridian on YouTube",
href: "#youtube",
icon: (
<path d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
),
},
];
const KEYFRAMES = `
@keyframes footxct-sheen {
0% { transform: translateX(-130%) skewX(-12deg); }
60% { transform: translateX(130%) skewX(-12deg); }
100% { transform: translateX(130%) skewX(-12deg); }
}
@keyframes footxct-ping {
0% { transform: scale(1); opacity: 0.55; }
75%, 100% { transform: scale(2.4); opacity: 0; }
}
@keyframes footxct-rise {
from { transform: translateY(3px); }
to { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
.footxct-anim { animation: none !important; }
}
`;
export default function FootxCtaTop() {
const reduce = useReducedMotion();
const uid = useId();
const emailId = `${uid}-email`;
const hintId = `${uid}-hint`;
const errId = `${uid}-err`;
const regionId = `${uid}-region`;
const [email, setEmail] = useState("");
const [freq, setFreq] = useState<FreqValue>("weekly");
const [status, setStatus] = useState<Status>("idle");
const [region, setRegion] = useState("us");
const [openCols, setOpenCols] = useState<Record<string, boolean>>({});
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const value = email.trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
setStatus("error");
return;
}
setStatus("success");
}
function reset() {
setStatus("idle");
setEmail("");
}
function toggleCol(key: string) {
setOpenCols((prev) => ({ ...prev, [key]: !prev[key] }));
}
function scrollTop() {
window.scrollTo({ top: 0, behavior: reduce ? "auto" : "smooth" });
}
const cardMotion = reduce
? {}
: {
initial: { opacity: 0, y: 28 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, margin: "-80px" },
transition: { duration: 0.6, ease: "easeOut" as const },
};
const controlRing =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-indigo-700";
const linkRing =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 rounded-sm";
return (
<section
aria-labelledby={`${uid}-cta-title`}
className="relative w-full overflow-hidden bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100"
>
<style>{KEYFRAMES}</style>
{/* decorative atmosphere */}
<div aria-hidden className="pointer-events-none absolute inset-0">
<div className="absolute -left-24 top-8 h-72 w-72 rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-600/20" />
<div className="absolute right-0 top-40 h-80 w-80 rounded-full bg-violet-400/15 blur-3xl dark:bg-violet-700/15" />
<div
className="absolute inset-0 opacity-[0.35] dark:opacity-20"
style={{
backgroundImage:
"linear-gradient(to right, rgb(100 116 139 / 0.08) 1px, transparent 1px), linear-gradient(to bottom, rgb(100 116 139 / 0.08) 1px, transparent 1px)",
backgroundSize: "56px 56px",
maskImage:
"radial-gradient(ellipse 80% 60% at 50% 0%, black 30%, transparent 75%)",
WebkitMaskImage:
"radial-gradient(ellipse 80% 60% at 50% 0%, black 30%, transparent 75%)",
}}
/>
</div>
<div className="relative mx-auto w-full max-w-6xl px-6 py-20 sm:py-24">
{/* ============ CTA on top ============ */}
<motion.div
{...cardMotion}
className="relative overflow-hidden rounded-3xl bg-gradient-to-br from-indigo-600 via-violet-600 to-indigo-700 p-8 shadow-2xl shadow-indigo-500/25 ring-1 ring-inset ring-white/15 sm:p-12 dark:shadow-indigo-900/40"
>
{/* animated sheen */}
<span
aria-hidden
className="footxct-anim pointer-events-none absolute inset-y-0 -left-1/2 w-1/2 bg-gradient-to-r from-transparent via-white/25 to-transparent"
style={{ animation: "footxct-sheen 7s ease-in-out infinite" }}
/>
{/* soft corner glow */}
<span
aria-hidden
className="pointer-events-none absolute -right-16 -top-16 h-56 w-56 rounded-full bg-white/10 blur-2xl"
/>
<div className="relative grid gap-10 lg:grid-cols-2 lg:items-center">
<div>
<span className="inline-flex items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] text-indigo-50 ring-1 ring-inset ring-white/20">
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="M4 4h16v12H5.17L4 17.17V4z" />
</svg>
Product newsletter
</span>
<h2
id={`${uid}-cta-title`}
className="mt-5 text-3xl font-semibold leading-[1.1] tracking-tight text-white sm:text-4xl"
>
Get the playbook behind teams that ship every week.
</h2>
<p className="mt-4 max-w-md text-base leading-relaxed text-indigo-100">
Every Thursday we send one concrete tactic, a real product
teardown, and the Meridian changelog. No fluff, about four
minutes to read.
</p>
<div className="mt-6 flex items-center gap-3 text-sm text-indigo-100">
<div className="flex -space-x-2" aria-hidden>
{["bg-amber-300", "bg-emerald-300", "bg-rose-300", "bg-sky-300"].map(
(c, i) => (
<span
key={i}
className={`h-7 w-7 rounded-full ${c} ring-2 ring-indigo-600`}
/>
),
)}
</div>
<span>
Joined by <strong className="font-semibold text-white">14,200+</strong> product teams
</span>
</div>
</div>
{/* form / success */}
<div className="lg:pl-6">
{status === "success" ? (
<div
role="status"
className="footxct-anim rounded-2xl bg-white/10 p-6 ring-1 ring-inset ring-white/25"
style={{ animation: "footxct-rise 0.4s ease-out" }}
>
<div className="flex items-center gap-3">
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-emerald-400 text-emerald-950">
<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="M20 6 9 17l-5-5" />
</svg>
</span>
<p className="text-lg font-semibold text-white">You’re on the list.</p>
</div>
<p className="mt-3 text-sm text-indigo-100">
Confirmation sent to{" "}
<span className="font-medium text-white">{email.trim()}</span>. You’ll
get the {freq} edition, starting this{" "}
{freq === "weekly" ? "Thursday" : "month"}.
</p>
<button
type="button"
onClick={reset}
className={`mt-4 text-sm font-semibold text-white underline decoration-white/40 underline-offset-4 transition hover:decoration-white ${controlRing} rounded-sm`}
>
Use a different email
</button>
</div>
) : (
<form onSubmit={handleSubmit} noValidate className="space-y-4">
<div>
<label htmlFor={emailId} className="mb-1.5 block text-sm font-medium text-indigo-50">
Work email
</label>
<div className="flex flex-col gap-3 sm:flex-row">
<input
id={emailId}
type="email"
name="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" ? errId : hintId}
className={`w-full rounded-xl bg-white/10 px-4 py-3 text-white placeholder-white/55 ring-1 ring-inset transition ${
status === "error"
? "ring-rose-300 focus-visible:ring-rose-200"
: "ring-white/25"
} focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white`}
/>
<button
type="submit"
className={`shrink-0 rounded-xl bg-white px-5 py-3 text-sm font-semibold text-indigo-700 shadow-sm transition hover:bg-indigo-50 active:scale-[0.98] ${controlRing}`}
>
Subscribe
</button>
</div>
{status === "error" ? (
<p id={errId} role="alert" className="mt-2 flex items-center gap-1.5 text-sm text-rose-100">
<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 8v4M12 16h.01" />
</svg>
Please enter a valid email address.
</p>
) : (
<p id={hintId} className="mt-2 text-sm text-indigo-100/80">
One email, no spam. Unsubscribe in a click.
</p>
)}
</div>
<fieldset>
<legend className="mb-1.5 text-sm font-medium text-indigo-50">
Delivery frequency
</legend>
<div className="inline-flex rounded-xl bg-white/10 p-1 ring-1 ring-inset ring-white/20">
{FREQ_OPTIONS.map((opt) => (
<label key={opt.value} className="cursor-pointer">
<input
type="radio"
name={`${uid}-freq`}
value={opt.value}
checked={freq === opt.value}
onChange={() => setFreq(opt.value)}
className="peer sr-only"
/>
<span className="block rounded-lg px-4 py-1.5 text-sm font-medium text-indigo-100 transition peer-checked:bg-white peer-checked:text-indigo-700 peer-checked:shadow-sm peer-focus-visible:ring-2 peer-focus-visible:ring-white peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-indigo-700">
{opt.label}
</span>
</label>
))}
</div>
</fieldset>
</form>
)}
</div>
</div>
</motion.div>
{/* ============ Footer body ============ */}
<footer className="mt-16 grid gap-12 lg:grid-cols-12">
{/* brand */}
<div className="lg:col-span-4">
<a href="#top" className={`inline-flex items-center gap-2.5 ${linkRing}`}>
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-sm">
<svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="M3 18 9 8l4 6 3-4 2 8" />
</svg>
</span>
<span className="text-lg font-semibold tracking-tight text-slate-900 dark:text-white">
Meridian
</span>
</a>
<p className="mt-4 max-w-xs text-sm leading-relaxed text-slate-600 dark:text-slate-400">
The planning workspace where product, design, and engineering
track one roadmap, from first spec to shipped release.
</p>
<p className="mt-5 inline-flex items-center gap-2.5 rounded-full bg-emerald-50 px-3 py-1.5 text-sm font-medium text-emerald-700 ring-1 ring-inset ring-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-500/25">
<span className="relative flex h-2.5 w-2.5">
<span
className="footxct-anim absolute inline-flex h-full w-full rounded-full bg-emerald-400"
style={{ animation: "footxct-ping 2.4s cubic-bezier(0,0,0.2,1) infinite" }}
/>
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-emerald-500" />
</span>
All systems operational
</p>
</div>
{/* link columns */}
<div className="grid grid-cols-1 gap-x-8 gap-y-2 sm:grid-cols-2 lg:col-span-8 lg:grid-cols-4">
{FOOTER_COLUMNS.map((col) => {
const open = openCols[col.key] ?? false;
const panelId = `${uid}-${col.key}`;
return (
<div key={col.key} className="border-b border-slate-200 py-2 sm:border-none sm:py-0 dark:border-slate-800">
<h3>
<button
type="button"
onClick={() => toggleCol(col.key)}
aria-expanded={open}
aria-controls={panelId}
className={`flex w-full items-center justify-between py-2 text-sm font-semibold text-slate-900 sm:pointer-events-none sm:cursor-default sm:py-0 dark:text-white ${linkRing}`}
>
{col.title}
<svg
viewBox="0 0 24 24"
className={`h-4 w-4 text-slate-400 transition-transform sm:hidden ${open ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="m6 9 6 6 6-6" />
</svg>
</button>
</h3>
<ul
id={panelId}
className={`space-y-3 pb-3 pt-1 sm:mt-4 sm:block sm:pb-0 ${open ? "block" : "hidden"}`}
>
{col.links.map((link) => (
<li key={link.label}>
<a
href={link.href}
className={`text-sm text-slate-600 transition hover:text-indigo-600 dark:text-slate-400 dark:hover:text-indigo-300 ${linkRing}`}
>
{link.label}
</a>
</li>
))}
</ul>
</div>
);
})}
</div>
</footer>
{/* ============ Bottom bar ============ */}
<div className="mt-12 flex flex-col gap-6 border-t border-slate-200 pt-8 dark:border-slate-800 md:flex-row md:items-center md:justify-between">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:gap-6">
<p className="text-sm text-slate-500 dark:text-slate-500">
© 2026 Meridian Labs, Inc.
</p>
<nav aria-label="Legal">
<ul className="flex flex-wrap gap-x-5 gap-y-2 text-sm">
{[
{ label: "Privacy", href: "#privacy" },
{ label: "Terms", href: "#terms" },
{ label: "Cookies", href: "#cookies" },
{ label: "Sitemap", href: "#sitemap" },
].map((l) => (
<li key={l.label}>
<a
href={l.href}
className={`text-slate-500 transition hover:text-slate-900 dark:text-slate-500 dark:hover:text-slate-200 ${linkRing}`}
>
{l.label}
</a>
</li>
))}
</ul>
</nav>
</div>
<div className="flex items-center gap-4">
<ul className="flex items-center gap-1.5">
{SOCIALS.map((s) => (
<li key={s.label}>
<a
href={s.href}
aria-label={s.label}
className={`flex h-9 w-9 items-center justify-center rounded-lg text-slate-500 transition hover:bg-slate-200/70 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white ${linkRing}`}
>
<svg viewBox="0 0 24 24" className="h-[18px] w-[18px]" fill="currentColor" aria-hidden>
{s.icon}
</svg>
</a>
</li>
))}
</ul>
<div className="relative">
<label htmlFor={regionId} className="sr-only">
Region and language
</label>
<svg viewBox="0 0 24 24" className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<circle cx="12" cy="12" r="9" />
<path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 3a15 15 0 0 0 0 18" />
</svg>
<select
id={regionId}
value={region}
onChange={(e) => setRegion(e.target.value)}
className={`appearance-none rounded-lg border border-slate-300 bg-white py-2 pl-8 pr-8 text-sm font-medium text-slate-700 transition hover:border-slate-400 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:border-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950`}
>
<option value="us">United States (EN)</option>
<option value="uk">United Kingdom (EN)</option>
<option value="de">Deutschland (DE)</option>
<option value="fr">France (FR)</option>
<option value="jp">日本 (JA)</option>
</select>
<svg viewBox="0 0 24 24" className="pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="m6 9 6 6 6-6" />
</svg>
</div>
<button
type="button"
onClick={scrollTop}
className={`flex h-9 items-center gap-1.5 rounded-lg border border-slate-300 px-3 text-sm font-medium text-slate-600 transition hover:border-slate-400 hover:text-slate-900 dark:border-slate-700 dark:text-slate-300 dark:hover:border-slate-600 dark:hover:text-white ${linkRing}`}
>
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="m18 15-6-6-6 6" />
</svg>
Top
</button>
</div>
</div>
</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 →
Multi Column Footer With Newsletter
OriginalA brand block with an inline email newsletter signup sits beside three navigation columns, over a bottom bar with copyright and social icons.

Minimal Single Row Footer
OriginalA compact single-row footer that centres a wordmark, inline link list and social icons above a slim copyright line.

Dark Footer With Oversized Brand
OriginalA dark footer pairing four compact link columns with an oversized gradient brand wordmark and a live status indicator.

Sitemap Style Footer
OriginalA five-column sitemap-style footer with a brand intro and contact CTA above a full site index and a legal bottom bar.

Columns Footer
Originalmulti-column footer

Newsletter Footer
Originalfooter with newsletter

Minimal Footer
Originalminimal centred footer

Dark Footer
Originaldark footer with columns

Social Footer
Originalfooter with social icons

App Links Footer
Originalfooter with app store links

Sitemap Footer
Originallarge sitemap footer

Gradient Footer
Originalgradient footer

