Minimal Footer
Original · freeminimal centred 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-minimal.json"use client";
import { useCallback, useId, useRef, useState } from "react";
import type { ChangeEvent, FormEvent } from "react";
import { motion, useReducedMotion } from "motion/react";
type SubscribeStatus = "idle" | "invalid" | "success";
const NAV_LINKS: ReadonlyArray<{ label: string; href: string }> = [
{ label: "Product", href: "#product" },
{ label: "Changelog", href: "#changelog" },
{ label: "Docs", href: "#docs" },
{ label: "Pricing", href: "#pricing" },
{ label: "Careers", href: "#careers" },
];
const SOCIAL_LINKS: ReadonlyArray<{
label: string;
href: string;
path: string;
}> = [
{
label: "Halcyon on X",
href: "https://x.com",
path: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.66l-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",
},
{
label: "Halcyon on GitHub",
href: "https://github.com",
path: "M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2Z",
},
{
label: "Halcyon on LinkedIn",
href: "https://linkedin.com",
path: "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286ZM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065Zm1.782 13.019H3.555V9h3.564v11.452ZM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003Z",
},
];
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
export default function FootxMinimal() {
const prefersReducedMotion = useReducedMotion();
const [email, setEmail] = useState("");
const [status, setStatus] = useState<SubscribeStatus>("idle");
const inputRef = useRef<HTMLInputElement>(null);
const feedbackId = useId();
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setEmail(event.target.value);
setStatus((prev) => (prev === "idle" ? prev : "idle"));
}, []);
const handleSubmit = useCallback(
(event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!EMAIL_PATTERN.test(email.trim())) {
setStatus("invalid");
inputRef.current?.focus();
return;
}
setStatus("success");
setEmail("");
},
[email]
);
const scrollToTop = useCallback(() => {
if (typeof window === "undefined") return;
window.scrollTo({
top: 0,
behavior: prefersReducedMotion ? "auto" : "smooth",
});
}, [prefersReducedMotion]);
const year = new Date().getFullYear();
const fadeInitial = prefersReducedMotion
? { opacity: 1, y: 0 }
: { opacity: 0, y: 12 };
return (
<section
className="relative w-full overflow-hidden bg-white px-6 py-16 text-neutral-600 dark:bg-neutral-950 dark:text-neutral-400 sm:py-20"
aria-labelledby="footxmin-heading"
>
<style>{`
@keyframes footxmin-aura {
0%, 100% { opacity: 0.55; transform: scale(1); }
50% { opacity: 0.9; transform: scale(1.08); }
}
@keyframes footxmin-sheen {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
@media (prefers-reduced-motion: reduce) {
.footxmin-aura,
.footxmin-sheen { animation: none !important; }
}
`}</style>
{/* Ambient top divider */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-indigo-400/50 to-transparent dark:via-violet-400/40"
/>
<motion.div
initial={fadeInitial}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ duration: prefersReducedMotion ? 0 : 0.5, ease: "easeOut" }}
className="mx-auto flex max-w-xl flex-col items-center text-center"
>
{/* Logo mark */}
<div className="relative mb-6">
<span
aria-hidden="true"
className="footxmin-aura absolute inset-0 -z-10 rounded-full bg-indigo-500/30 blur-xl dark:bg-violet-500/30"
style={{ animation: "footxmin-aura 5s ease-in-out infinite" }}
/>
<a
href="#top"
onClick={(event) => {
event.preventDefault();
scrollToTop();
}}
aria-label="Halcyon — back to top"
className="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-neutral-900 text-white outline-none ring-offset-2 ring-offset-white transition-transform hover:-translate-y-0.5 focus-visible:ring-2 focus-visible:ring-indigo-500 dark:bg-white dark:text-neutral-950 dark:ring-offset-neutral-950 dark:focus-visible:ring-violet-400"
>
<svg
viewBox="0 0 24 24"
className="h-6 w-6"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M4 17 10 5l4 8 2-4 4 8z" />
</svg>
</a>
</div>
<h2
id="footxmin-heading"
className="text-lg font-semibold tracking-tight text-neutral-900 dark:text-neutral-50"
>
Halcyon
</h2>
<p className="mt-2 max-w-sm text-sm leading-relaxed text-neutral-500 dark:text-neutral-400">
The calm workspace where product teams turn scattered notes into
shipped features.
</p>
{/* Primary navigation */}
<nav aria-label="Footer" className="mt-8">
<ul className="flex flex-wrap items-center justify-center gap-x-6 gap-y-3">
{NAV_LINKS.map((link) => (
<li key={link.href}>
<a
href={link.href}
className="rounded-sm text-sm font-medium text-neutral-600 outline-none transition-colors hover:text-neutral-900 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-neutral-400 dark:hover:text-neutral-50 dark:focus-visible:ring-violet-400 dark:focus-visible:ring-offset-neutral-950"
>
{link.label}
</a>
</li>
))}
</ul>
</nav>
{/* Newsletter */}
<form
onSubmit={handleSubmit}
noValidate
className="mt-10 w-full max-w-sm"
>
<label
htmlFor="footxmin-email"
className="block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Get the monthly build log
</label>
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-500">
One email a month. No fluff, unsubscribe anytime.
</p>
<div className="mt-3 flex flex-col gap-2 sm:flex-row">
<input
ref={inputRef}
id="footxmin-email"
type="email"
name="email"
inputMode="email"
autoComplete="email"
placeholder="you@company.com"
value={email}
onChange={handleChange}
aria-invalid={status === "invalid"}
aria-describedby={feedbackId}
className={`min-w-0 flex-1 rounded-lg border bg-white px-3.5 py-2.5 text-sm text-neutral-900 shadow-sm outline-none transition-colors placeholder:text-neutral-400 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-neutral-900 dark:text-neutral-50 dark:placeholder:text-neutral-500 dark:focus-visible:ring-offset-neutral-950 ${
status === "invalid"
? "border-rose-400 focus-visible:ring-rose-500 dark:border-rose-500/70"
: "border-neutral-300 focus-visible:ring-indigo-500 dark:border-neutral-700 dark:focus-visible:ring-violet-400"
}`}
/>
<button
type="submit"
className="footxmin-sheen inline-flex items-center justify-center rounded-lg px-4 py-2.5 text-sm font-semibold text-white outline-none ring-offset-2 ring-offset-white transition-transform hover:-translate-y-0.5 focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-neutral-950 dark:ring-offset-neutral-950 dark:focus-visible:ring-violet-400"
style={{
backgroundImage:
"linear-gradient(110deg, #4f46e5, #7c3aed, #4f46e5)",
backgroundSize: "200% 100%",
animation: prefersReducedMotion
? "none"
: "footxmin-sheen 4s linear infinite",
}}
>
Subscribe
</button>
</div>
<p
id={feedbackId}
role="status"
aria-live="polite"
className={`mt-2 min-h-5 text-left text-xs ${
status === "invalid"
? "text-rose-600 dark:text-rose-400"
: status === "success"
? "text-emerald-600 dark:text-emerald-400"
: "text-transparent"
}`}
>
{status === "invalid"
? "Please enter a valid email address."
: status === "success"
? "You're in. Watch for the next build log."
: ""}
</p>
</form>
{/* Social */}
<ul className="mt-8 flex items-center justify-center gap-2">
{SOCIAL_LINKS.map((social) => (
<li key={social.href}>
<a
href={social.href}
target="_blank"
rel="noopener"
aria-label={social.label}
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-neutral-200 text-neutral-500 outline-none transition-colors hover:border-neutral-300 hover:text-neutral-900 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-neutral-800 dark:text-neutral-400 dark:hover:border-neutral-700 dark:hover:text-neutral-50 dark:focus-visible:ring-violet-400 dark:focus-visible:ring-offset-neutral-950"
>
<svg
viewBox="0 0 24 24"
className="h-4 w-4"
fill="currentColor"
aria-hidden="true"
>
<path d={social.path} />
</svg>
</a>
</li>
))}
</ul>
{/* Bottom bar */}
<div className="mt-10 flex w-full flex-col items-center gap-4 border-t border-neutral-200 pt-6 text-xs text-neutral-500 dark:border-neutral-800 dark:text-neutral-500 sm:flex-row sm:justify-between">
<p>© {year} Halcyon Labs, Inc. All rights reserved.</p>
<button
type="button"
onClick={scrollToTop}
className="group inline-flex items-center gap-1.5 rounded-md px-2 py-1 font-medium text-neutral-600 outline-none transition-colors hover:text-neutral-900 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-neutral-400 dark:hover:text-neutral-50 dark:focus-visible:ring-violet-400 dark:focus-visible:ring-offset-neutral-950"
>
Back to top
<svg
viewBox="0 0 24 24"
className="h-3.5 w-3.5 transition-transform group-hover:-translate-y-0.5"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M12 19V5M5 12l7-7 7 7" />
</svg>
</button>
</div>
</motion.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.

Columns Footer
Originalmulti-column footer

Newsletter Footer
Originalfooter with newsletter

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

