freeNew174 lines · framer-motion
Footer Minimal
One clean row, a hairline that glows. A minimal single-row footer: an invented wordmark, a short row of legal links, abstract social icons, and a copyright line, on lots of whitespace above a hairline border with a slow-travelling gradient streak.
- Use it for
- The closing row of any marketing page or docs site that wants a quiet, confident footer without a big sitemap.
- Themes
- Light + dark
- License
- Commercial use
Live
© 2026 Lumio. All rights reserved.
tsx
"use client";
import * as React from "react";
import { motion, useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils";
function useReducedMotionSafe() {
const prefersReduced = useReducedMotion();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
return mounted ? !!prefersReduced : false;
}
/**
* A minimal single-row footer: small invented wordmark on one side, a short
* row of legal/nav links, abstract social icons, and a copyright line — all
* on one thin bar with a subtle animated gradient hairline on top and a soft
* glow behind the mark so the block never reads as bare.
*/
export interface FooterLink {
label: string;
href?: string;
}
export interface FooterSocial {
label: string;
href?: string;
/** Which abstract glyph to draw. */
icon?: "orbit" | "wave" | "node" | "spark";
}
export interface FooterMinimalProps {
wordmark?: string;
links?: FooterLink[];
socials?: FooterSocial[];
copyright?: string;
className?: string;
}
const DEFAULT_LINKS: FooterLink[] = [
{ label: "Privacy" },
{ label: "Terms" },
{ label: "Status" },
{ label: "Contact" },
];
const DEFAULT_SOCIALS: FooterSocial[] = [
{ label: "Orbit", icon: "orbit" },
{ label: "Wave", icon: "wave" },
{ label: "Node", icon: "node" },
{ label: "Spark", icon: "spark" },
];
function Wordmark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 32 32" className={cn("size-7", className)} aria-hidden>
<rect x="1" y="1" width="30" height="30" rx="10" className="fill-primary" />
<path
d="M9 21V11.5L16 20l7-8.5V21"
fill="none"
className="stroke-primary-foreground"
strokeWidth="2.1"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function SocialIcon({ icon }: { icon: FooterSocial["icon"] }) {
switch (icon) {
case "wave":
return (
<svg viewBox="0 0 20 20" className="size-4" aria-hidden>
<path d="M2 13c1.5-3 3.5-3 5 0s3.5 3 5 0 3.5-3 5 0" fill="none" className="stroke-current" strokeWidth="1.6" strokeLinecap="round" />
</svg>
);
case "node":
return (
<svg viewBox="0 0 20 20" className="size-4" aria-hidden>
<circle cx="5" cy="6" r="2.1" className="fill-current" />
<circle cx="15" cy="6" r="2.1" className="fill-current" />
<circle cx="10" cy="15" r="2.1" className="fill-current" />
<path d="M6.6 7.4 8.6 13.4M13.4 7.4 11.4 13.4M7 6H13" className="stroke-current" strokeWidth="1.3" />
</svg>
);
case "spark":
return (
<svg viewBox="0 0 20 20" className="size-4" aria-hidden>
<path d="M10 2v5M10 13v5M2 10h5M13 10h5M4.5 4.5l3 3M12.5 12.5l3 3M4.5 15.5l3-3M12.5 7.5l3-3" className="stroke-current" strokeWidth="1.6" strokeLinecap="round" />
</svg>
);
default:
return (
<svg viewBox="0 0 20 20" className="size-4" aria-hidden>
<circle cx="10" cy="10" r="7.2" fill="none" className="stroke-current" strokeWidth="1.6" />
<ellipse cx="10" cy="10" rx="7.2" ry="3" fill="none" className="stroke-current" strokeWidth="1.6" />
</svg>
);
}
}
export function FooterMinimal({
wordmark = "Lumio",
links = DEFAULT_LINKS,
socials = DEFAULT_SOCIALS,
copyright = "© 2026 Lumio. All rights reserved.",
className,
}: FooterMinimalProps) {
const reduce = useReducedMotionSafe();
return (
<section className={cn("relative w-full bg-background", className)}>
<div className="relative h-px w-full overflow-hidden bg-border" aria-hidden>
<motion.div
className="absolute inset-y-0 w-1/3 bg-[linear-gradient(90deg,transparent,var(--signal),transparent)]"
animate={reduce ? undefined : { x: ["-40%", "340%"] }}
transition={reduce ? undefined : { duration: 6, repeat: Infinity, ease: "linear" }}
/>
</div>
<div className="relative mx-auto max-w-6xl px-4 py-10 sm:px-6 lg:px-10">
<div
className="pointer-events-none absolute left-4 top-1/2 size-24 -translate-y-1/2 rounded-full bg-signal/10 blur-2xl sm:left-6 lg:left-10"
aria-hidden
/>
<div className="relative flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between">
<a
href="#"
onClick={(e) => e.preventDefault()}
className="inline-flex items-center gap-2.5 font-display text-[1.05rem] font-extrabold tracking-[-0.03em] text-foreground"
>
<Wordmark />
{wordmark}
</a>
<nav aria-label="Legal" className="flex flex-wrap items-center justify-center gap-x-6 gap-y-2">
{links.map((l) => (
<a
key={l.label}
href={l.href ?? "#"}
onClick={(e) => (!l.href || l.href === "#") && e.preventDefault()}
className="text-sm font-medium text-muted-foreground transition hover:text-foreground"
>
{l.label}
</a>
))}
</nav>
<div className="flex items-center justify-center gap-2 sm:justify-end">
{socials.map((s) => (
<a
key={s.label}
href={s.href ?? "#"}
onClick={(e) => (!s.href || s.href === "#") && e.preventDefault()}
aria-label={s.label}
className="inline-flex size-9 items-center justify-center rounded-full border border-border text-muted-foreground transition hover:border-foreground/40 hover:text-foreground"
>
<SocialIcon icon={s.icon} />
</a>
))}
</div>
</div>
<p className="relative mt-8 text-center text-xs text-muted-foreground sm:text-left">{copyright}</p>
</div>
</section>
);
}
export default FooterMinimal;
How it behaves
- →A 1px top border holds a short gradient streak (transparent → --signal → transparent) that loops across its width on a slow linear animation; prefers-reduced-motion freezes it in place.
- →A soft, static signal-coloured glow sits behind the wordmark corner so the row doesn't read as bare, without competing with the text.
- →Layout is a single flex row (wordmark, legal links, social icons) that stacks into three centred groups on narrow widths instead of wrapping awkwardly.
- →Legal links and social icons are data-driven (links, socials props) so the row can be relabelled without touching markup.
Accessibility
- ✓The legal link row is a labelled `<nav aria-label="Legal">`; every link and social icon is a real, keyboard-reachable `<a>` with visible hover/focus states.
- ✓Each social icon button has an `aria-label` naming the platform; the glyphs inside are `aria-hidden`.
- ✓The animated gradient streak and background glow are `aria-hidden` and purely decorative.
- ✓Text keeps muted-foreground/foreground contrast in both themes; nothing relies on the signal accent alone to convey meaning.
- ✓Honours prefers-reduced-motion.