Sitemap Footer
Original · freelarge sitemap footer
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"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 & 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
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

CTA Top Footer
Originalfooter with a CTA on top

Social Footer
Originalfooter with social icons

App Links Footer
Originalfooter with app store links

Gradient Footer
Originalgradient footer

