Skip to content
Black FridayBlack Friday launch price: Lifetime $149 until 30 Nov. Regular price $299 from 1 Dec.ends in 15d 02:06:09Get Lifetime →
loopkit
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.