Web InnoventixFreeCode

CTA Top Footer

Original · free

footer with a CTA on top

byWeb InnoventixReact + Tailwind
footxctatopfooters
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/footx-cta-top.json
footx-cta-top.tsx
"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&rsquo;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&rsquo;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">
              &copy; 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

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 →