Web InnoventixFreeCode

Gradient Footer

Original · free

gradient footer

byWeb InnoventixReact + Tailwind
footxgradientfooters
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-gradient.json
footx-gradient.tsx
"use client";

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

type FooterLink = { label: string; href: string };
type FooterColumn = { id: string; title: string; links: FooterLink[] };
type SocialLink = { name: string; href: string; icon: ReactNode };
type SubscribeState = "idle" | "invalid" | "success";

const COLUMNS: FooterColumn[] = [
  {
    id: "product",
    title: "Product",
    links: [
      { label: "Uptime monitors", href: "#monitors" },
      { label: "Status pages", href: "#status-pages" },
      { label: "Incident timeline", href: "#incidents" },
      { label: "Integrations", href: "#integrations" },
      { label: "Pricing", href: "#pricing" },
    ],
  },
  {
    id: "developers",
    title: "Developers",
    links: [
      { label: "REST & webhook API", href: "#api" },
      { label: "CLI reference", href: "#cli" },
      { label: "Terraform provider", href: "#terraform" },
      { label: "Changelog", href: "#changelog" },
      { label: "Service status", href: "#service-status" },
    ],
  },
  {
    id: "company",
    title: "Company",
    links: [
      { label: "About Beacon", href: "#about" },
      { label: "Careers — 6 open roles", href: "#careers" },
      { label: "Engineering blog", href: "#blog" },
      { label: "Press kit", href: "#press" },
      { label: "Contact sales", href: "#contact" },
    ],
  },
  {
    id: "legal",
    title: "Legal",
    links: [
      { label: "Privacy policy", href: "#privacy" },
      { label: "Terms of service", href: "#terms" },
      { label: "Data processing (DPA)", href: "#dpa" },
      { label: "99.99% SLA", href: "#sla" },
      { label: "Trust & security", href: "#security" },
    ],
  },
];

const SOCIALS: SocialLink[] = [
  {
    name: "Beacon on X",
    href: "https://x.com",
    icon: (
      <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className="h-[18px] w-[18px]">
        <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.657l-5.214-6.817-5.966 6.817H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231 5.45-6.231Zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77Z" />
      </svg>
    ),
  },
  {
    name: "Beacon on GitHub",
    href: "https://github.com",
    icon: (
      <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className="h-5 w-5">
        <path d="M12 .5A11.5 11.5 0 0 0 .5 12a11.5 11.5 0 0 0 7.86 10.92c.58.1.79-.25.79-.56v-2c-3.2.7-3.88-1.37-3.88-1.37-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.2 1.77 1.2 1.03 1.77 2.7 1.26 3.36.96.1-.75.4-1.26.73-1.55-2.56-.29-5.25-1.28-5.25-5.7 0-1.26.45-2.29 1.19-3.1-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11 11 0 0 1 5.79 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.11 3.05.74.81 1.19 1.84 1.19 3.1 0 4.43-2.69 5.4-5.26 5.69.41.36.78 1.06.78 2.14v3.17c0 .31.21.67.8.56A11.5 11.5 0 0 0 23.5 12 11.5 11.5 0 0 0 12 .5Z" />
      </svg>
    ),
  },
  {
    name: "Beacon on LinkedIn",
    href: "https://linkedin.com",
    icon: (
      <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className="h-5 w-5">
        <path d="M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.13 1.44-2.13 2.94v5.67H9.35V9h3.42v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28ZM5.34 7.43a2.07 2.07 0 1 1 0-4.14 2.07 2.07 0 0 1 0 4.14ZM7.12 20.45H3.55V9h3.57v11.45ZM22.22 0H1.77C.8 0 0 .78 0 1.74v20.52C0 23.22.8 24 1.77 24h20.45c.98 0 1.78-.78 1.78-1.74V1.74C24 .78 23.2 0 22.22 0Z" />
      </svg>
    ),
  },
  {
    name: "Beacon on YouTube",
    href: "https://youtube.com",
    icon: (
      <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className="h-5 w-5">
        <path d="M23.5 6.2a3.02 3.02 0 0 0-2.12-2.14C19.5 3.55 12 3.55 12 3.55s-7.5 0-9.38.51A3.02 3.02 0 0 0 .5 6.2 31.5 31.5 0 0 0 0 12a31.5 31.5 0 0 0 .5 5.8 3.02 3.02 0 0 0 2.12 2.14c1.88.51 9.38.51 9.38.51s7.5 0 9.38-.51a3.02 3.02 0 0 0 2.12-2.14A31.5 31.5 0 0 0 24 12a31.5 31.5 0 0 0-.5-5.8ZM9.6 15.57V8.43L15.8 12l-6.2 3.57Z" />
      </svg>
    ),
  },
  {
    name: "Beacon RSS feed",
    href: "#rss",
    icon: (
      <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className="h-[18px] w-[18px]">
        <path d="M4.5 15.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM3 8.5v3a9 9 0 0 1 9 9h3A12 12 0 0 0 3 8.5ZM3 3v3a15 15 0 0 1 15 15h3A18 18 0 0 0 3 3Z" />
      </svg>
    ),
  },
];

const REGIONS: FooterLink[] = [
  { label: "US East (Virginia)", href: "us-east" },
  { label: "EU West (Frankfurt)", href: "eu-west" },
  { label: "Asia (Singapore)", href: "asia-se" },
  { label: "South America (São Paulo)", href: "sa-east" },
];

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export default function FootxGradient() {
  const reduce = useReducedMotion();
  const baseId = useId();
  const [email, setEmail] = useState("");
  const [status, setStatus] = useState<SubscribeState>("idle");
  const [region, setRegion] = useState(REGIONS[0].href);
  const [open, setOpen] = useState<Record<string, boolean>>({});
  const inputRef = useRef<HTMLInputElement>(null);

  const emailId = `${baseId}-email`;
  const emailMsgId = `${baseId}-email-msg`;

  const toggleColumn = (id: string) =>
    setOpen((prev) => ({ ...prev, [id]: !prev[id] }));

  const handleSubscribe = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (!EMAIL_RE.test(email.trim())) {
      setStatus("invalid");
      inputRef.current?.focus();
      return;
    }
    setStatus("success");
    setEmail("");
  };

  const scrollTop = () =>
    window.scrollTo({ top: 0, behavior: reduce ? "auto" : "smooth" });

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-br from-indigo-100 via-violet-100 to-sky-100 text-slate-700 dark:from-indigo-950 dark:via-violet-950 dark:to-slate-950 dark:text-slate-300">
      <style>{`
        @keyframes footx-aurora-a {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
          50% { transform: translate3d(7%, -5%, 0) scale(1.18); }
        }
        @keyframes footx-aurora-b {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1.12); }
          50% { transform: translate3d(-6%, 6%, 0) scale(1); }
        }
        @keyframes footx-shine {
          0% { background-position: 0% 50%; }
          100% { background-position: 200% 50%; }
        }
        @keyframes footx-pulse-dot {
          0%, 100% { opacity: 1; transform: scale(1); }
          50% { opacity: 0.5; transform: scale(0.78); }
        }
        @keyframes footx-ring {
          0% { transform: scale(0.6); opacity: 0.7; }
          100% { transform: scale(2.4); opacity: 0; }
        }
        .footx-aurora-a { animation: footx-aurora-a 20s ease-in-out infinite; }
        .footx-aurora-b { animation: footx-aurora-b 26s ease-in-out infinite; }
        .footx-shine {
          background-size: 200% auto;
          animation: footx-shine 7s linear infinite;
        }
        .footx-dot { animation: footx-pulse-dot 2.4s ease-in-out infinite; }
        .footx-ring { animation: footx-ring 2.4s ease-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .footx-aurora-a, .footx-aurora-b, .footx-shine, .footx-dot, .footx-ring {
            animation: none !important;
          }
        }
      `}</style>

      {/* Decorative animated gradient field */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
        <div className="footx-aurora-a absolute -left-24 -top-24 h-[28rem] w-[28rem] rounded-full bg-indigo-400/40 blur-3xl dark:bg-indigo-600/25" />
        <div className="footx-aurora-b absolute -right-20 top-10 h-[26rem] w-[26rem] rounded-full bg-sky-400/40 blur-3xl dark:bg-sky-600/20" />
        <div className="footx-aurora-a absolute bottom-0 left-1/3 h-[24rem] w-[24rem] rounded-full bg-violet-400/30 blur-3xl dark:bg-violet-600/25" />
      </div>
      <div
        aria-hidden="true"
        className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-indigo-400/70 to-transparent dark:via-violet-400/60"
      />

      <div className="relative mx-auto w-full max-w-7xl px-6 py-16 sm:px-8 sm:py-20 lg:px-10">
        <motion.div
          initial={reduce ? false : { opacity: 0, y: 24 }}
          whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
          viewport={{ once: true, margin: "-80px" }}
          transition={{ duration: 0.6, ease: "easeOut" }}
          className="grid gap-12 lg:grid-cols-12"
        >
          {/* Brand + newsletter */}
          <div className="lg:col-span-4">
            <a
              href="#top"
              className="inline-flex items-center gap-3 rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-indigo-100 dark:focus-visible:ring-offset-slate-950"
            >
              <span className="grid h-10 w-10 place-items-center rounded-xl bg-gradient-to-br from-indigo-500 via-violet-500 to-sky-500 shadow-lg shadow-indigo-500/30">
                <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-6 w-6 text-white">
                  <path
                    d="M12 2v6m0 8v6m10-10h-6M8 12H2m14.95-7.07-4.24 4.24m-1.42 1.42-4.24 4.24m0-9.9 4.24 4.24m1.42 1.42 4.24 4.24"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                  />
                </svg>
              </span>
              <span className="footx-shine bg-gradient-to-r from-indigo-600 via-violet-600 to-sky-600 bg-clip-text text-2xl font-semibold tracking-tight text-transparent dark:from-indigo-300 dark:via-violet-300 dark:to-sky-300">
                Beacon
              </span>
            </a>

            <p className="mt-5 max-w-sm text-sm leading-relaxed text-slate-600 dark:text-slate-400">
              Uptime monitoring, on-call alerts, and branded status pages for teams
              who ship every day. Watching 41,200+ endpoints across five continents.
            </p>

            <form
              onSubmit={handleSubscribe}
              noValidate
              className="mt-7 max-w-sm"
              aria-describedby={status !== "idle" ? emailMsgId : undefined}
            >
              <label
                htmlFor={emailId}
                className="block text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400"
              >
                Monthly incident report
              </label>
              <div className="mt-2 flex flex-col gap-2 sm:flex-row">
                <input
                  ref={inputRef}
                  id={emailId}
                  type="email"
                  name="email"
                  autoComplete="email"
                  inputMode="email"
                  placeholder="you@company.com"
                  value={email}
                  aria-invalid={status === "invalid"}
                  aria-describedby={status !== "idle" ? emailMsgId : undefined}
                  onChange={(e) => {
                    setEmail(e.target.value);
                    if (status !== "idle") setStatus("idle");
                  }}
                  className="w-full rounded-lg border border-slate-300/80 bg-white/80 px-3.5 py-2.5 text-sm text-slate-900 placeholder:text-slate-400 shadow-sm backdrop-blur transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-indigo-100 aria-[invalid=true]:border-rose-400 dark:border-white/10 dark:bg-white/5 dark:text-white dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-950"
                />
                <button
                  type="submit"
                  className="shrink-0 rounded-lg bg-gradient-to-r from-indigo-500 to-violet-500 px-4 py-2.5 text-sm font-semibold text-white shadow-md shadow-indigo-500/25 transition hover:from-indigo-600 hover:to-violet-600 active:translate-y-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-indigo-100 dark:focus-visible:ring-offset-slate-950"
                >
                  Subscribe
                </button>
              </div>
              <p
                id={emailMsgId}
                role="status"
                aria-live="polite"
                className={`mt-2 min-h-[1.25rem] text-xs ${
                  status === "invalid"
                    ? "text-rose-600 dark:text-rose-400"
                    : status === "success"
                      ? "text-emerald-600 dark:text-emerald-400"
                      : "text-slate-500 dark:text-slate-500"
                }`}
              >
                {status === "invalid"
                  ? "Please enter a valid email address."
                  : status === "success"
                    ? "You're in — the next report lands on the 1st. No spam."
                    : "One email a month. Real incidents, honest postmortems."}
              </p>
            </form>

            <ul className="mt-7 flex items-center gap-2">
              {SOCIALS.map((social) => (
                <li key={social.name}>
                  <a
                    href={social.href}
                    target="_blank"
                    rel="noopener"
                    aria-label={social.name}
                    className="grid h-10 w-10 place-items-center rounded-lg border border-slate-300/70 bg-white/60 text-slate-600 shadow-sm backdrop-blur transition hover:-translate-y-0.5 hover:border-indigo-400 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-indigo-100 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:border-violet-400 dark:hover:text-violet-300 dark:focus-visible:ring-offset-slate-950"
                  >
                    {social.icon}
                  </a>
                </li>
              ))}
            </ul>
          </div>

          {/* Link columns */}
          <nav
            aria-label="Footer"
            className="grid grid-cols-1 gap-x-8 gap-y-2 sm:grid-cols-2 lg:col-span-8 lg:grid-cols-4 lg:gap-y-0"
          >
            {COLUMNS.map((col) => {
              const panelId = `${baseId}-panel-${col.id}`;
              const isOpen = !!open[col.id];
              return (
                <div key={col.id} className="border-b border-slate-300/60 py-2 sm:border-none sm:py-0 dark:border-white/10">
                  <h3>
                    <button
                      type="button"
                      onClick={() => toggleColumn(col.id)}
                      aria-expanded={isOpen}
                      aria-controls={panelId}
                      className="flex w-full items-center justify-between gap-2 py-2 text-left text-sm font-semibold text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-indigo-100 sm:cursor-default sm:py-0 dark:text-white dark:focus-visible:ring-offset-slate-950"
                    >
                      <span>{col.title}</span>
                      <svg
                        viewBox="0 0 24 24"
                        fill="none"
                        aria-hidden="true"
                        className={`h-4 w-4 shrink-0 text-slate-500 transition-transform duration-200 sm:hidden ${
                          isOpen ? "rotate-180" : ""
                        }`}
                      >
                        <path d="m6 9 6 6 6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                      </svg>
                    </button>
                  </h3>
                  <ul
                    id={panelId}
                    className={`${isOpen ? "grid" : "hidden"} gap-1 pb-2 sm:grid sm:pb-0 sm:pt-4`}
                  >
                    {col.links.map((link) => (
                      <li key={link.label}>
                        <a
                          href={link.href}
                          className="inline-block rounded py-1.5 text-sm text-slate-600 transition hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-indigo-100 dark:text-slate-400 dark:hover:text-violet-300 dark:focus-visible:ring-offset-slate-950"
                        >
                          {link.label}
                        </a>
                      </li>
                    ))}
                  </ul>
                </div>
              );
            })}
          </nav>
        </motion.div>

        {/* Bottom bar */}
        <div className="mt-14 flex flex-col gap-6 border-t border-slate-300/60 pt-8 dark:border-white/10 lg:flex-row lg:items-center lg:justify-between">
          <div className="flex flex-wrap items-center gap-x-6 gap-y-3">
            <span className="inline-flex items-center gap-2 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-3 py-1 text-xs font-medium text-emerald-700 dark:text-emerald-300">
              <span className="relative flex h-2 w-2">
                <span className="footx-ring absolute inline-flex h-full w-full rounded-full bg-emerald-500" aria-hidden="true" />
                <span className="footx-dot relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
              </span>
              All systems operational
            </span>

            <div className="flex items-center gap-2">
              <label
                htmlFor={`${baseId}-region`}
                className="text-xs font-medium text-slate-500 dark:text-slate-400"
              >
                Region
              </label>
              <select
                id={`${baseId}-region`}
                value={region}
                onChange={(e) => setRegion(e.target.value)}
                className="rounded-lg border border-slate-300/70 bg-white/70 px-2.5 py-1.5 text-xs font-medium text-slate-700 shadow-sm backdrop-blur transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-indigo-100 dark:border-white/10 dark:bg-white/5 dark:text-slate-200 dark:focus-visible:ring-offset-slate-950"
              >
                {REGIONS.map((r) => (
                  <option key={r.href} value={r.href} className="text-slate-900">
                    {r.label}
                  </option>
                ))}
              </select>
            </div>
          </div>

          <div className="flex items-center gap-5">
            <p className="text-xs text-slate-500 dark:text-slate-500">
              © {new Date().getFullYear()} Beacon Systems, Inc. Built in the open.
            </p>
            <button
              type="button"
              onClick={scrollTop}
              className="inline-flex items-center gap-1.5 rounded-lg border border-slate-300/70 bg-white/60 px-3 py-1.5 text-xs font-medium text-slate-700 shadow-sm backdrop-blur transition hover:-translate-y-0.5 hover:border-indigo-400 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-indigo-100 dark:border-white/10 dark:bg-white/5 dark:text-slate-200 dark:hover:border-violet-400 dark:hover:text-violet-200 dark:focus-visible:ring-offset-slate-950"
            >
              Back to top
              <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-3.5 w-3.5">
                <path d="M12 19V5m0 0-6 6m6-6 6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
            </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 →