Columns Footer
Original · freemulti-column 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-columns.json"use client";
import { useId, useState, type ChangeEvent, type FormEvent, type ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type FooterLink = { label: string; href: string; badge?: string };
type FooterColumn = { id: string; title: string; links: FooterLink[] };
type Social = { label: string; href: string; icon: ReactNode };
type Region = { value: string; label: string; currency: string };
const COLUMNS: FooterColumn[] = [
{
id: "product",
title: "Product",
links: [
{ label: "Status pages", href: "#" },
{ label: "Uptime checks", href: "#" },
{ label: "Incident timeline", href: "#" },
{ label: "On-call schedules", href: "#" },
{ label: "Integrations", href: "#" },
{ label: "Changelog", href: "#", badge: "New" },
],
},
{
id: "developers",
title: "Developers",
links: [
{ label: "Documentation", href: "#" },
{ label: "API reference", href: "#" },
{ label: "Webhooks", href: "#" },
{ label: "CLI & Terraform", href: "#" },
{ label: "Open source", href: "#" },
{ label: "Community", href: "#" },
],
},
{
id: "company",
title: "Company",
links: [
{ label: "About us", href: "#" },
{ label: "Careers", href: "#", badge: "9" },
{ label: "Customers", href: "#" },
{ label: "Blog", href: "#" },
{ label: "Security", href: "#" },
{ label: "Contact", href: "#" },
],
},
{
id: "legal",
title: "Legal",
links: [
{ label: "Terms of service", href: "#" },
{ label: "Privacy policy", href: "#" },
{ label: "Data processing", href: "#" },
{ label: "Subprocessors", href: "#" },
{ label: "SLA", href: "#" },
{ label: "Cookie settings", href: "#" },
],
},
];
const REGIONS: Region[] = [
{ value: "us", label: "United States", currency: "USD $" },
{ value: "uk", label: "United Kingdom", currency: "GBP £" },
{ value: "eu", label: "Eurozone", currency: "EUR €" },
{ value: "in", label: "India", currency: "INR ₹" },
{ value: "au", label: "Australia", currency: "AUD $" },
];
function GitHubIcon(): ReactNode {
return (
<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.3-1.7-1.3-1.7-1.06-.72.08-.71.08-.71 1.17.08 1.79 1.2 1.79 1.2 1.04 1.79 2.73 1.27 3.4.97.1-.76.4-1.27.74-1.56-2.56-.29-5.26-1.28-5.26-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.8 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.7 5.4-5.28 5.69.42.36.79 1.06.79 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>
);
}
function XIcon(): ReactNode {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className="h-[18px] w-[18px]">
<path d="M18.24 2.25h3.31l-7.23 8.26 8.5 11.24h-6.65l-5.21-6.82-5.97 6.82H1.68l7.73-8.84L1.25 2.25h6.82l4.71 6.23 5.46-6.23Zm-1.16 17.52h1.83L7.02 4.13H5.05L17.08 19.77Z" />
</svg>
);
}
function LinkedInIcon(): ReactNode {
return (
<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.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.55V9h3.57v11.45ZM22.22 0H1.77C.8 0 0 .78 0 1.75v20.5C0 23.22.79 24 1.77 24h20.45c.98 0 1.78-.78 1.78-1.75V1.75C24 .78 23.2 0 22.22 0Z" />
</svg>
);
}
function YouTubeIcon(): ReactNode {
return (
<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.2C0 8.09 0 12 0 12s0 3.91.5 5.8a3.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.14c.5-1.89.5-5.8.5-5.8s0-3.91-.5-5.8ZM9.6 15.6V8.4l6.25 3.6L9.6 15.6Z" />
</svg>
);
}
function RssIcon(): ReactNode {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className="h-[18px] w-[18px]">
<path d="M4 11a9 9 0 0 1 9 9h2.5A11.5 11.5 0 0 0 4 8.5V11Zm0 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Zm0-10a15 15 0 0 1 15 15h2.5A17.5 17.5 0 0 0 4 3.5V6Z" />
</svg>
);
}
const SOCIALS: Social[] = [
{ label: "GitHub", href: "https://github.com", icon: <GitHubIcon /> },
{ label: "X (Twitter)", href: "https://x.com", icon: <XIcon /> },
{ label: "LinkedIn", href: "https://linkedin.com", icon: <LinkedInIcon /> },
{ label: "YouTube", href: "https://youtube.com", icon: <YouTubeIcon /> },
{ label: "RSS feed", href: "#", icon: <RssIcon /> },
];
const FOCUS =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950";
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export default function FootxColumns(): ReactNode {
const reduce = useReducedMotion();
const uid = useId();
const [email, setEmail] = useState("");
const [status, setStatus] = useState<"idle" | "error" | "success">("idle");
const [open, setOpen] = useState<Record<string, boolean>>({});
const [region, setRegion] = useState<string>("us");
const emailId = `${uid}-email`;
const msgId = `${uid}-msg`;
const activeRegion = REGIONS.find((r) => r.value === region) ?? REGIONS[0];
function handleSubmit(e: FormEvent<HTMLFormElement>): void {
e.preventDefault();
if (!EMAIL_RE.test(email.trim())) {
setStatus("error");
return;
}
setStatus("success");
}
function handleRegion(e: ChangeEvent<HTMLSelectElement>): void {
setRegion(e.target.value);
}
function toggle(id: string): void {
setOpen((prev) => ({ ...prev, [id]: !prev[id] }));
}
function toTop(): void {
window.scrollTo({ top: 0, behavior: reduce ? "auto" : "smooth" });
}
return (
<section
className="relative w-full overflow-hidden border-t border-slate-200 bg-slate-50 text-slate-700 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-300"
aria-labelledby={`${uid}-footer-heading`}
>
<style>{`
@keyframes footx-ping {
75%, 100% { transform: scale(2.4); opacity: 0; }
}
@keyframes footx-sheen {
0% { background-position: -140% 0; }
100% { background-position: 240% 0; }
}
.footx-ping { animation: footx-ping 2s cubic-bezier(0,0,0.2,1) infinite; }
.footx-mark {
background-image: linear-gradient(115deg, transparent 30%, rgba(255,255,255,0.55) 50%, transparent 70%);
background-size: 220% 100%;
animation: footx-sheen 5.5s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.footx-ping, .footx-mark { animation: none !important; }
}
`}</style>
{/* Decorative atmosphere */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<div className="absolute -left-32 -top-40 h-80 w-80 rounded-full bg-indigo-300/25 blur-3xl dark:bg-indigo-600/15" />
<div className="absolute -right-24 top-10 h-72 w-72 rounded-full bg-violet-300/20 blur-3xl dark:bg-violet-700/15" />
<div
className="absolute inset-0 opacity-[0.5] dark:opacity-[0.35]"
style={{
backgroundImage:
"linear-gradient(to right, rgba(100,116,139,0.09) 1px, transparent 1px), linear-gradient(to bottom, rgba(100,116,139,0.09) 1px, transparent 1px)",
backgroundSize: "56px 56px",
maskImage: "radial-gradient(ellipse 90% 60% at 50% 0%, #000 40%, transparent 100%)",
WebkitMaskImage: "radial-gradient(ellipse 90% 60% at 50% 0%, #000 40%, transparent 100%)",
}}
/>
</div>
<div className="relative mx-auto max-w-7xl px-6 py-16 sm:px-8 sm:py-20">
<h2 id={`${uid}-footer-heading`} className="sr-only">
Pulsegrid site footer
</h2>
{/* Newsletter band */}
<motion.div
initial={reduce ? false : { opacity: 0, y: 18 }}
whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="flex flex-col gap-8 border-b border-slate-200 pb-12 dark:border-slate-800 lg:flex-row lg:items-center lg:justify-between"
>
<div className="max-w-md">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
The Incident Report
</p>
<h3 className="mt-3 text-2xl font-semibold tracking-tight text-slate-900 dark:text-white sm:text-3xl">
Learn from outages before they reach you
</h3>
<p className="mt-3 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
One email a month: a real postmortem, a monitoring pattern worth stealing, and what
changed in Pulsegrid. Read by 41,000 on-call engineers.
</p>
</div>
<form onSubmit={handleSubmit} noValidate className="w-full lg:max-w-md">
<label htmlFor={emailId} className="sr-only">
Work email address
</label>
<div className="flex flex-col gap-3 sm:flex-row">
<input
id={emailId}
name="email"
type="email"
autoComplete="email"
inputMode="email"
placeholder="you@company.com"
value={email}
aria-invalid={status === "error"}
aria-describedby={msgId}
onChange={(e) => {
setEmail(e.target.value);
if (status !== "idle") setStatus("idle");
}}
className={`w-full rounded-xl border bg-white px-4 py-3 text-sm text-slate-900 placeholder:text-slate-400 dark:bg-slate-900 dark:text-white dark:placeholder:text-slate-500 ${FOCUS} ${
status === "error"
? "border-rose-400 dark:border-rose-500/70"
: "border-slate-300 dark:border-slate-700"
}`}
/>
<button
type="submit"
className={`inline-flex shrink-0 items-center justify-center gap-2 rounded-xl bg-slate-900 px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-indigo-600 dark:bg-white dark:text-slate-900 dark:hover:bg-indigo-400 ${FOCUS} dark:focus-visible:ring-offset-slate-950`}
>
Subscribe
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="h-4 w-4">
<path
d="M4 10h11m0 0-4-4m4 4-4 4"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>
<p
id={msgId}
role="status"
aria-live="polite"
className={`mt-2.5 flex items-center gap-1.5 text-xs ${
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"
}`}
>
{status === "success" ? (
<>
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" className="h-4 w-4">
<path
fillRule="evenodd"
d="M16.7 5.3a1 1 0 0 1 0 1.4l-7.5 7.5a1 1 0 0 1-1.4 0l-3.5-3.5a1 1 0 1 1 1.4-1.4l2.8 2.79 6.8-6.79a1 1 0 0 1 1.4 0Z"
clipRule="evenodd"
/>
</svg>
You're in. Check your inbox to confirm.
</>
) : status === "error" ? (
"Please enter a valid email address."
) : (
"Monthly. No spam. Unsubscribe in one click."
)}
</p>
</form>
</motion.div>
{/* Columns */}
<div className="grid grid-cols-2 gap-x-8 gap-y-10 pt-14 sm:grid-cols-2 lg:grid-cols-6">
{/* Brand block */}
<div className="col-span-2 lg:col-span-2">
<a
href="#"
className={`inline-flex items-center gap-2.5 rounded-lg ${FOCUS}`}
aria-label="Pulsegrid home"
>
<span className="footx-mark flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-lg shadow-indigo-500/25">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-5 w-5">
<path
d="M2 13h4l2.5-7 4 14 3-11 2 4h4.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
<span className="text-lg font-semibold tracking-tight text-slate-900 dark:text-white">
Pulsegrid
</span>
</a>
<p className="mt-4 max-w-xs text-sm leading-relaxed text-slate-600 dark:text-slate-400">
Uptime monitoring, status pages, and on-call scheduling that engineering teams
actually trust when the pager goes off.
</p>
<div className="mt-5 inline-flex items-center gap-2 rounded-full border border-emerald-300/60 bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-300">
<span className="relative flex h-2.5 w-2.5" aria-hidden="true">
<span className="footx-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-emerald-500" />
</span>
All systems operational
</div>
<ul className="mt-6 flex flex-wrap gap-2.5">
{SOCIALS.map((s) => (
<li key={s.label}>
<a
href={s.href}
target="_blank"
rel="noopener"
aria-label={s.label}
className={`inline-flex h-10 w-10 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-500 transition-colors hover:border-indigo-400 hover:text-indigo-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400 dark:hover:border-indigo-500 dark:hover:text-indigo-400 ${FOCUS}`}
>
{s.icon}
</a>
</li>
))}
</ul>
</div>
{/* Link columns — accordion on mobile, static on sm+ */}
{COLUMNS.map((col) => {
const isOpen = !!open[col.id];
const panelId = `${uid}-${col.id}-panel`;
return (
<div key={col.id} className="border-t border-slate-200 py-2 dark:border-slate-800 sm:border-t-0 sm:py-0">
<h3>
<button
type="button"
onClick={() => toggle(col.id)}
aria-expanded={isOpen}
aria-controls={panelId}
className={`flex w-full items-center justify-between rounded-md py-2 text-left text-sm font-semibold text-slate-900 dark:text-white sm:pointer-events-none sm:cursor-default sm:py-0 ${FOCUS}`}
>
{col.title}
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
className={`h-4 w-4 text-slate-400 transition-transform duration-200 sm:hidden ${
isOpen ? "rotate-180" : ""
}`}
>
<path
d="m5 7.5 5 5 5-5"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</h3>
<ul
id={panelId}
className={`space-y-3 pb-2 pt-1 sm:mt-4 sm:block sm:pb-0 sm:pt-0 ${
isOpen ? "block" : "hidden"
}`}
>
{col.links.map((link) => (
<li key={link.label}>
<a
href={link.href}
className={`group inline-flex items-center gap-2 rounded-md text-sm text-slate-600 transition-colors hover:text-indigo-600 dark:text-slate-400 dark:hover:text-indigo-400 ${FOCUS}`}
>
{link.label}
{link.badge ? (
<span className="rounded-full bg-indigo-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase leading-none tracking-wide text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300">
{link.badge}
</span>
) : null}
</a>
</li>
))}
</ul>
</div>
);
})}
</div>
{/* Bottom bar */}
<div className="mt-14 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-1 text-xs text-slate-500 dark:text-slate-500 sm:flex-row sm:items-center sm:gap-4">
<span>© 2026 Pulsegrid, Inc. All rights reserved.</span>
<span className="hidden sm:inline" aria-hidden="true">
·
</span>
<span>
Prices shown in{" "}
<span className="font-medium text-slate-700 dark:text-slate-300">
{activeRegion.currency}
</span>
</span>
</div>
<div className="flex flex-wrap items-center gap-3">
<label htmlFor={`${uid}-region`} className="text-xs text-slate-500 dark:text-slate-500">
Region
</label>
<div className="relative">
<select
id={`${uid}-region`}
value={region}
onChange={handleRegion}
className={`appearance-none rounded-lg border border-slate-300 bg-white py-2 pl-3 pr-9 text-xs font-medium text-slate-700 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 ${FOCUS}`}
>
{REGIONS.map((r) => (
<option key={r.value} value={r.value}>
{r.label}
</option>
))}
</select>
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
className="pointer-events-none absolute right-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400"
>
<path
d="m5 7.5 5 5 5-5"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<button
type="button"
onClick={toTop}
className={`inline-flex items-center gap-1.5 rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-700 transition-colors hover:border-indigo-400 hover:text-indigo-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:border-indigo-500 dark:hover:text-indigo-400 ${FOCUS}`}
>
Back to top
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="h-4 w-4">
<path
d="m5 12.5 5-5 5 5"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Multi Column Footer With Newsletter
OriginalA brand block with an inline email newsletter signup sits beside three navigation columns, over a bottom bar with copyright and social icons.

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

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

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

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

Sitemap Footer
Originallarge sitemap footer

Gradient Footer
Originalgradient footer

