Web InnoventixFreeCode

Sitemap Footer

Original · free

large sitemap footer

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

import {
  type FormEvent,
  type KeyboardEvent as ReactKeyboardEvent,
  type ReactNode,
  useEffect,
  useId,
  useRef,
  useState,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type FooterLink = { label: string; href: string; badge?: "New" | "Beta" };
type FooterColumn = { id: string; title: string; links: FooterLink[] };
type Region = { code: string; label: string; currency: string };
type Social = { name: string; href: string; icon: ReactNode };

const COLUMNS: FooterColumn[] = [
  {
    id: "product",
    title: "Product",
    links: [
      { label: "Platform overview", href: "#overview" },
      { label: "Pricing", href: "#pricing" },
      { label: "Integrations", href: "#integrations" },
      { label: "Security & trust", href: "#security" },
      { label: "Changelog", href: "#changelog" },
      { label: "Roadmap", href: "#roadmap", badge: "New" },
    ],
  },
  {
    id: "solutions",
    title: "Solutions",
    links: [
      { label: "For startups", href: "#startups" },
      { label: "For enterprise", href: "#enterprise" },
      { label: "For data teams", href: "#data-teams" },
      { label: "E-commerce", href: "#ecommerce" },
      { label: "Fintech", href: "#fintech" },
      { label: "Healthcare", href: "#healthcare" },
    ],
  },
  {
    id: "developers",
    title: "Developers",
    links: [
      { label: "Documentation", href: "#docs" },
      { label: "API reference", href: "#api" },
      { label: "SDKs & libraries", href: "#sdks", badge: "Beta" },
      { label: "Command line", href: "#cli" },
      { label: "System status", href: "#status" },
      { label: "Open source", href: "#oss" },
    ],
  },
  {
    id: "resources",
    title: "Resources",
    links: [
      { label: "Blog", href: "#blog" },
      { label: "Engineering guides", href: "#guides" },
      { label: "Webinars", href: "#webinars" },
      { label: "Templates", href: "#templates" },
      { label: "Help center", href: "#help" },
      { label: "Community forum", href: "#community" },
    ],
  },
  {
    id: "company",
    title: "Company",
    links: [
      { label: "About us", href: "#about" },
      { label: "Careers", href: "#careers", badge: "New" },
      { label: "Customers", href: "#customers" },
      { label: "Press kit", href: "#press" },
      { label: "Partners", href: "#partners" },
      { label: "Contact sales", href: "#contact" },
    ],
  },
];

const REGIONS: Region[] = [
  { code: "us", label: "United States", currency: "USD" },
  { code: "gb", label: "United Kingdom", currency: "GBP" },
  { code: "eu", label: "Eurozone", currency: "EUR" },
  { code: "ca", label: "Canada", currency: "CAD" },
  { code: "au", label: "Australia", currency: "AUD" },
  { code: "jp", label: "Japan", currency: "JPY" },
  { code: "in", label: "India", currency: "INR" },
  { code: "br", label: "Brazil", currency: "BRL" },
];

const LEGAL: FooterLink[] = [
  { label: "Privacy Policy", href: "#privacy" },
  { label: "Terms of Service", href: "#terms" },
  { label: "Cookie Preferences", href: "#cookies" },
  { label: "Data Processing", href: "#dpa" },
  { label: "Sitemap", href: "#sitemap" },
];

const SOCIALS: Social[] = [
  {
    name: "GitHub",
    href: "#github",
    icon: (
      <svg viewBox="0 0 24 24" fill="currentColor" className="h-[18px] w-[18px]" aria-hidden="true">
        <path d="M12 .5C5.37.5 0 5.87 0 12.5c0 5.3 3.44 9.8 8.21 11.39.6.11.82-.26.82-.58 0-.29-.01-1.05-.02-2.06-3.34.73-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.75.08-.73.08-.73 1.21.09 1.84 1.24 1.84 1.24 1.07 1.84 2.81 1.31 3.5 1 .11-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.12-.3-.54-1.52.12-3.17 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.29-1.55 3.3-1.23 3.3-1.23.66 1.65.24 2.87.12 3.17.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.62-5.49 5.92.43.37.82 1.1.82 2.22 0 1.61-.02 2.9-.02 3.29 0 .32.22.7.83.58A12 12 0 0 0 24 12.5C24 5.87 18.63.5 12 .5Z" />
      </svg>
    ),
  },
  {
    name: "X",
    href: "#x",
    icon: (
      <svg viewBox="0 0 24 24" fill="currentColor" className="h-[18px] w-[18px]" aria-hidden="true">
        <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.656l-5.214-6.817L4.99 21.75H1.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: "LinkedIn",
    href: "#linkedin",
    icon: (
      <svg viewBox="0 0 24 24" fill="currentColor" className="h-[18px] w-[18px]" aria-hidden="true">
        <path d="M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 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.06 2.06 0 1 1 0-4.13 2.06 2.06 0 0 1 0 4.13ZM7.12 20.45H3.56V9h3.56v11.45ZM22.22 0H1.77C.8 0 0 .78 0 1.75v20.5C0 23.22.8 24 1.77 24h20.45c.98 0 1.78-.78 1.78-1.75V1.75C24 .78 23.2 0 22.22 0Z" />
      </svg>
    ),
  },
  {
    name: "YouTube",
    href: "#youtube",
    icon: (
      <svg viewBox="0 0 24 24" fill="currentColor" className="h-[18px] w-[18px]" aria-hidden="true">
        <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.6 31.6 0 0 0 0 12a31.6 31.6 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.6 31.6 0 0 0 24 12a31.6 31.6 0 0 0-.5-5.8ZM9.6 15.6V8.4l6.2 3.6-6.2 3.6Z" />
      </svg>
    ),
  },
  {
    name: "RSS feed",
    href: "#rss",
    icon: (
      <svg viewBox="0 0 24 24" fill="currentColor" className="h-[18px] w-[18px]" aria-hidden="true">
        <path d="M4.26 11.25a8.49 8.49 0 0 1 8.49 8.49h2.4A10.89 10.89 0 0 0 4.26 8.85v2.4Zm0-4.77c7.35 0 13.26 5.96 13.26 13.26h2.4C19.92 11.03 12.97 4.08 4.26 4.08v2.4ZM6.2 18.06a1.94 1.94 0 1 0 0-3.88 1.94 1.94 0 0 0 0 3.88Z" />
      </svg>
    ),
  },
];

function useIsWideViewport(): boolean {
  const [wide, setWide] = useState(false);
  useEffect(() => {
    const mq = window.matchMedia("(min-width: 640px)");
    const update = () => setWide(mq.matches);
    update();
    mq.addEventListener("change", update);
    return () => mq.removeEventListener("change", update);
  }, []);
  return wide;
}

function ChevronIcon({ open }: { open: boolean }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={`h-4 w-4 shrink-0 transition-transform duration-300 ${open ? "rotate-180" : "rotate-0"}`}
    >
      <path d="m6 9 6 6 6-6" />
    </svg>
  );
}

function GlobeIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.7}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className="h-4 w-4 shrink-0"
    >
      <circle cx="12" cy="12" r="9" />
      <path d="M3 12h18M12 3c2.5 2.6 3.8 5.8 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.8-3.8-9s1.3-6.4 3.8-9Z" />
    </svg>
  );
}

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2.4}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="m5 12.5 4.5 4.5L19 6.5" />
    </svg>
  );
}

function badgeClasses(badge: "New" | "Beta"): string {
  if (badge === "New") {
    return "bg-emerald-100 text-emerald-700 dark:bg-emerald-400/15 dark:text-emerald-300";
  }
  return "bg-amber-100 text-amber-700 dark:bg-amber-400/15 dark:text-amber-300";
}

function LinkBadge({ badge }: { badge: "New" | "Beta" }) {
  return (
    <span
      className={`rounded-full px-1.5 py-px text-[10px] font-semibold uppercase leading-tight tracking-wide ${badgeClasses(
        badge,
      )}`}
    >
      {badge}
    </span>
  );
}

function RegionSelect({
  value,
  onChange,
  reduced,
}: {
  value: string;
  onChange: (code: string) => void;
  reduced: boolean;
}) {
  const uid = useId();
  const [open, setOpen] = useState(false);
  const selectedIndex = Math.max(
    0,
    REGIONS.findIndex((r) => r.code === value),
  );
  const [activeIndex, setActiveIndex] = useState(selectedIndex);
  const containerRef = useRef<HTMLDivElement>(null);
  const buttonRef = useRef<HTMLButtonElement>(null);
  const listRef = useRef<HTMLUListElement>(null);
  const current = REGIONS[selectedIndex];

  const openMenu = () => {
    setActiveIndex(selectedIndex);
    setOpen(true);
  };

  useEffect(() => {
    if (open) listRef.current?.focus();
  }, [open]);

  useEffect(() => {
    if (!open) return;
    const onPointer = (event: MouseEvent) => {
      if (!containerRef.current?.contains(event.target as Node)) {
        setOpen(false);
      }
    };
    document.addEventListener("mousedown", onPointer);
    return () => document.removeEventListener("mousedown", onPointer);
  }, [open]);

  const commit = (index: number) => {
    const next = REGIONS[index];
    if (next) onChange(next.code);
    setOpen(false);
    buttonRef.current?.focus();
  };

  const onButtonKeyDown = (event: ReactKeyboardEvent<HTMLButtonElement>) => {
    if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
      event.preventDefault();
      openMenu();
    }
  };

  const onListKeyDown = (event: ReactKeyboardEvent<HTMLUListElement>) => {
    switch (event.key) {
      case "ArrowDown":
        event.preventDefault();
        setActiveIndex((i) => Math.min(REGIONS.length - 1, i + 1));
        break;
      case "ArrowUp":
        event.preventDefault();
        setActiveIndex((i) => Math.max(0, i - 1));
        break;
      case "Home":
        event.preventDefault();
        setActiveIndex(0);
        break;
      case "End":
        event.preventDefault();
        setActiveIndex(REGIONS.length - 1);
        break;
      case "Enter":
      case " ":
        event.preventDefault();
        commit(activeIndex);
        break;
      case "Escape":
        event.preventDefault();
        setOpen(false);
        buttonRef.current?.focus();
        break;
      case "Tab":
        setOpen(false);
        break;
      default:
        break;
    }
  };

  return (
    <div ref={containerRef} className="relative w-full max-w-[16rem]">
      <span id={`${uid}-label`} className="sr-only">
        Choose your region and currency
      </span>
      <button
        ref={buttonRef}
        type="button"
        id={`${uid}-btn`}
        aria-haspopup="listbox"
        aria-expanded={open}
        aria-labelledby={`${uid}-label ${uid}-btn`}
        onClick={() => (open ? setOpen(false) : openMenu())}
        onKeyDown={onButtonKeyDown}
        className="flex w-full items-center gap-2 rounded-xl border border-slate-300 bg-white px-3.5 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:border-slate-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:border-slate-600 dark:focus-visible:ring-offset-slate-950"
      >
        <GlobeIcon />
        <span className="flex-1 truncate text-left">
          {current.label}
          <span className="text-slate-400 dark:text-slate-500"> · {current.currency}</span>
        </span>
        <ChevronIcon open={open} />
      </button>

      <AnimatePresence>
        {open && (
          <motion.ul
            ref={listRef}
            role="listbox"
            tabIndex={-1}
            aria-labelledby={`${uid}-label`}
            aria-activedescendant={`${uid}-opt-${activeIndex}`}
            onKeyDown={onListKeyDown}
            initial={reduced ? { opacity: 0 } : { opacity: 0, y: -8, scale: 0.97 }}
            animate={{ opacity: 1, y: 0, scale: 1 }}
            exit={reduced ? { opacity: 0 } : { opacity: 0, y: -8, scale: 0.97 }}
            transition={{ duration: reduced ? 0.12 : 0.18, ease: [0.16, 1, 0.3, 1] }}
            className="absolute bottom-full z-20 mb-2 max-h-64 w-full overflow-auto rounded-xl border border-slate-200 bg-white p-1.5 shadow-xl shadow-slate-900/10 focus-visible:outline-none dark:border-slate-700 dark:bg-slate-900 dark:shadow-black/40"
          >
            {REGIONS.map((region, index) => {
              const isSelected = index === selectedIndex;
              const isActive = index === activeIndex;
              return (
                <li
                  key={region.code}
                  id={`${uid}-opt-${index}`}
                  role="option"
                  aria-selected={isSelected}
                  onClick={() => commit(index)}
                  onMouseEnter={() => setActiveIndex(index)}
                  className={`flex cursor-pointer items-center justify-between gap-3 rounded-lg px-3 py-2 text-sm ${
                    isActive
                      ? "bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-200"
                      : "text-slate-700 dark:text-slate-300"
                  }`}
                >
                  <span className="truncate">{region.label}</span>
                  <span className="flex items-center gap-2">
                    <span className="text-xs tabular-nums text-slate-400 dark:text-slate-500">
                      {region.currency}
                    </span>
                    {isSelected && (
                      <CheckIcon className="h-4 w-4 text-indigo-600 dark:text-indigo-300" />
                    )}
                  </span>
                </li>
              );
            })}
          </motion.ul>
        )}
      </AnimatePresence>
    </div>
  );
}

export default function FootxSitemap() {
  const reduced = useReducedMotion() ?? false;
  const isWide = useIsWideViewport();
  const uid = useId();
  const [openSections, setOpenSections] = useState<string[]>([]);
  const [region, setRegion] = useState("us");
  const [email, setEmail] = useState("");
  const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
  const [message, setMessage] = useState("");

  const toggleSection = (id: string) =>
    setOpenSections((prev) =>
      prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id],
    );

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
    if (!valid) {
      setStatus("error");
      setMessage("Please enter a valid email address so we can reach you.");
      return;
    }
    setStatus("success");
    setMessage("You're on the list. Check your inbox to confirm the subscription.");
    setEmail("");
  };

  const keyframes = `
@keyframes footx-status-ring {
  0% { transform: scale(0.9); opacity: 0.65; }
  70% { transform: scale(2.4); opacity: 0; }
  100% { transform: scale(2.4); opacity: 0; }
}
@keyframes footx-sheen {
  0% { background-position: -160% 0; }
  100% { background-position: 260% 0; }
}
@keyframes footx-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-14px); }
}
.footx-status-ring { animation: footx-status-ring 2.6s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
.footx-sheen { background-size: 220% 100%; animation: footx-sheen 7s linear infinite; }
.footx-float-a { animation: footx-float 13s ease-in-out infinite; }
.footx-float-b { animation: footx-float 17s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .footx-status-ring, .footx-sheen, .footx-float-a, .footx-float-b { animation: none !important; }
}
`;

  return (
    <section className="relative w-full overflow-hidden border-t border-slate-200 bg-slate-50 text-slate-600 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-400">
      <style>{keyframes}</style>

      {/* decorative atmosphere */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
        <div className="footx-float-a absolute -left-24 top-10 h-72 w-72 rounded-full bg-indigo-400/15 blur-3xl dark:bg-indigo-500/10" />
        <div className="footx-float-b absolute -right-16 bottom-0 h-80 w-80 rounded-full bg-violet-400/15 blur-3xl dark:bg-sky-500/10" />
        <div
          className="absolute inset-0 opacity-[0.4] dark:opacity-[0.25]"
          style={{
            backgroundImage:
              "radial-gradient(circle at 1px 1px, rgb(100 116 139 / 0.18) 1px, transparent 0)",
            backgroundSize: "34px 34px",
            maskImage: "linear-gradient(to bottom, transparent, black 22%, black 78%, transparent)",
            WebkitMaskImage:
              "linear-gradient(to bottom, transparent, black 22%, black 78%, transparent)",
          }}
        />
      </div>

      <footer className="relative mx-auto max-w-7xl px-6 py-16 sm:px-8 lg:py-24">
        <h2 className="sr-only">Nimbus site footer</h2>

        {/* Top band: brand + newsletter */}
        <div className="grid gap-12 lg:grid-cols-[1.05fr_1fr] lg:gap-16">
          <div className="flex flex-col gap-7">
            <a
              href="#top"
              className="inline-flex w-fit items-center gap-3 rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-4 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-950"
            >
              <span className="footx-sheen inline-flex h-11 w-11 items-center justify-center rounded-xl bg-gradient-to-br from-indigo-500 via-violet-500 to-sky-500 shadow-lg shadow-indigo-500/25">
                <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-6 w-6 text-white">
                  <circle cx="6" cy="6" r="2.1" fill="currentColor" />
                  <circle cx="18" cy="6" r="2.1" fill="currentColor" />
                  <circle cx="12" cy="12" r="2.1" fill="currentColor" />
                  <circle cx="6" cy="18" r="2.1" fill="currentColor" />
                  <circle cx="18" cy="18" r="2.1" fill="currentColor" />
                  <path
                    d="M6 6 12 12M18 6 12 12M12 12 6 18M12 12 18 18"
                    stroke="currentColor"
                    strokeWidth="1.4"
                    strokeLinecap="round"
                    opacity="0.85"
                  />
                </svg>
              </span>
              <span className="text-xl font-semibold tracking-tight text-slate-900 dark:text-white">
                Nimbus
              </span>
            </a>

            <p className="max-w-md text-[15px] leading-relaxed text-slate-600 dark:text-slate-400">
              The data platform for teams who ship. Nimbus unifies your warehouse, streams, and
              product events into one queryable layer — so every decision runs on fresh, governed
              data.
            </p>

            <div className="flex flex-col gap-4">
              <RegionSelect value={region} onChange={setRegion} reduced={reduced} />

              <a
                href="#status"
                className="inline-flex w-fit items-center gap-2.5 rounded-full border border-slate-200 bg-white/70 py-1.5 pl-2.5 pr-4 text-sm text-slate-600 backdrop-blur transition-colors hover:border-emerald-300 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-800 dark:bg-slate-900/60 dark:text-slate-400 dark:hover:border-emerald-500/40 dark:hover:text-white dark:focus-visible:ring-offset-slate-950"
              >
                <span className="relative flex h-2.5 w-2.5 items-center justify-center">
                  <span className="footx-status-ring absolute inline-flex h-2.5 w-2.5 rounded-full bg-emerald-400" />
                  <span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-emerald-500" />
                </span>
                All systems operational
              </a>

              <div className="flex items-center gap-1.5 pt-1">
                {SOCIALS.map((social) => (
                  <a
                    key={social.name}
                    href={social.href}
                    aria-label={`Nimbus on ${social.name}`}
                    className="inline-flex h-9 w-9 items-center justify-center rounded-lg text-slate-500 transition-colors hover:bg-slate-200/70 hover: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-slate-50 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white dark:focus-visible:ring-offset-slate-950"
                  >
                    {social.icon}
                  </a>
                ))}
              </div>
            </div>
          </div>

          {/* Newsletter */}
          <div className="rounded-2xl border border-slate-200 bg-white/70 p-6 backdrop-blur-sm sm:p-8 dark:border-slate-800 dark:bg-slate-900/50">
            <h3 className="text-base font-semibold text-slate-900 dark:text-white">
              The Monthly Deploy
            </h3>
            <p className="mt-2 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
              Product updates, engineering deep-dives, and the occasional postmortem. One email a
              month — no fluff, unsubscribe in a click.
            </p>

            <form onSubmit={onSubmit} noValidate className="mt-6 flex flex-col gap-3">
              <label htmlFor={`${uid}-email`} className="sr-only">
                Email address
              </label>
              <div className="flex flex-col gap-3 sm:flex-row">
                <input
                  id={`${uid}-email`}
                  type="email"
                  name="email"
                  autoComplete="email"
                  inputMode="email"
                  placeholder="you@company.com"
                  value={email}
                  onChange={(event) => {
                    setEmail(event.target.value);
                    if (status !== "idle") {
                      setStatus("idle");
                      setMessage("");
                    }
                  }}
                  aria-invalid={status === "error"}
                  aria-describedby={`${uid}-msg`}
                  className={`w-full flex-1 rounded-xl border bg-white px-4 py-2.5 text-sm text-slate-900 placeholder:text-slate-400 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-slate-950/60 dark:text-white dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-900 ${
                    status === "error"
                      ? "border-rose-400 focus-visible:ring-rose-500 dark:border-rose-500/60"
                      : "border-slate-300 focus-visible:ring-indigo-500 dark:border-slate-700"
                  }`}
                />
                <button
                  type="submit"
                  className="shrink-0 rounded-xl bg-slate-900 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-indigo-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:bg-white dark:text-slate-900 dark:hover:bg-indigo-400 dark:hover:text-white dark:focus-visible:ring-offset-slate-900"
                >
                  Subscribe
                </button>
              </div>

              <p
                id={`${uid}-msg`}
                role="status"
                aria-live="polite"
                className={`min-h-[1.25rem] text-sm ${
                  status === "error"
                    ? "text-rose-600 dark:text-rose-400"
                    : status === "success"
                      ? "text-emerald-600 dark:text-emerald-400"
                      : "text-slate-500 dark:text-slate-500"
                }`}
              >
                <AnimatePresence mode="wait" initial={false}>
                  <motion.span
                    key={status + message}
                    initial={reduced ? { opacity: 0 } : { opacity: 0, y: 4 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={reduced ? { opacity: 0 } : { opacity: 0, y: -4 }}
                    transition={{ duration: 0.2 }}
                    className="inline-flex items-center gap-1.5"
                  >
                    {status === "success" && <CheckIcon className="h-4 w-4" />}
                    {message || "Join 24,000+ engineers already subscribed."}
                  </motion.span>
                </AnimatePresence>
              </p>
            </form>
          </div>
        </div>

        <hr className="my-12 border-slate-200 dark:border-slate-800 lg:my-16" />

        {/* Sitemap grid */}
        <nav aria-label="Footer" className="grid grid-cols-1 gap-x-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5">
          {COLUMNS.map((column) => {
            const isOpen = openSections.includes(column.id);
            const expanded = isWide || isOpen;
            const headId = `${uid}-head-${column.id}`;
            const panelId = `${uid}-panel-${column.id}`;

            const linkList = (
              <ul className="flex flex-col gap-3 pb-6 pt-1 sm:pb-0">
                {column.links.map((link) => (
                  <li key={link.href}>
                    <a
                      href={link.href}
                      className="inline-flex items-center gap-2 rounded text-sm text-slate-600 transition-colors 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-slate-50 dark:text-slate-400 dark:hover:text-white dark:focus-visible:ring-offset-slate-950"
                    >
                      {link.label}
                      {link.badge && <LinkBadge badge={link.badge} />}
                    </a>
                  </li>
                ))}
              </ul>
            );

            return (
              <div
                key={column.id}
                className="border-b border-slate-200 py-1 dark:border-slate-800 sm:border-b-0 sm:py-0"
              >
                {isWide ? (
                  <h3
                    id={headId}
                    className="mb-4 text-xs font-semibold uppercase tracking-wider text-slate-900 dark:text-slate-200"
                  >
                    {column.title}
                  </h3>
                ) : (
                  <h3>
                    <button
                      id={headId}
                      type="button"
                      aria-expanded={expanded}
                      aria-controls={panelId}
                      onClick={() => toggleSection(column.id)}
                      className="flex w-full items-center justify-between gap-2 py-3 text-sm font-semibold uppercase tracking-wider 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-slate-50 dark:text-slate-200 dark:focus-visible:ring-offset-slate-950"
                    >
                      <span>{column.title}</span>
                      <ChevronIcon open={expanded} />
                    </button>
                  </h3>
                )}

                {isWide ? (
                  <div id={panelId} role="region" aria-labelledby={headId}>
                    {linkList}
                  </div>
                ) : (
                  <AnimatePresence initial={false}>
                    {expanded && (
                      <motion.div
                        id={panelId}
                        role="region"
                        aria-labelledby={headId}
                        initial={{ height: 0, opacity: 0 }}
                        animate={{ height: "auto", opacity: 1 }}
                        exit={{ height: 0, opacity: 0 }}
                        transition={{ duration: reduced ? 0 : 0.26, ease: [0.16, 1, 0.3, 1] }}
                        className="overflow-hidden"
                      >
                        {linkList}
                      </motion.div>
                    )}
                  </AnimatePresence>
                )}
              </div>
            );
          })}
        </nav>

        <hr className="my-12 border-slate-200 dark:border-slate-800 lg:my-14" />

        {/* Bottom bar */}
        <div className="flex flex-col-reverse gap-6 md:flex-row md:items-center md:justify-between">
          <p className="text-sm text-slate-500 dark:text-slate-500">
            © 2026 Nimbus Data, Inc. Built in Amsterdam &amp; Austin.
          </p>
          <ul className="flex flex-wrap items-center gap-x-6 gap-y-3">
            {LEGAL.map((item) => (
              <li key={item.href}>
                <a
                  href={item.href}
                  className="text-sm text-slate-500 transition-colors hover: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-slate-50 dark:text-slate-500 dark:hover:text-white dark:focus-visible:ring-offset-slate-950"
                >
                  {item.label}
                </a>
              </li>
            ))}
          </ul>
        </div>
      </footer>
    </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 →