Web InnoventixFreeCode

Dark Footer

Original · free

dark footer with columns

byWeb InnoventixReact + Tailwind
footxdarkfooters
Open

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-dark.json
footx-dark.tsx
"use client";

import { useState, useId, useEffect, type FormEvent, type ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";

type SubscribeStatus = "idle" | "error" | "success";

type FooterLink = { label: string; href: string; badge?: string };
type FooterColumn = { title: string; links: FooterLink[] };

const COLUMNS: FooterColumn[] = [
  {
    title: "Product",
    links: [
      { label: "Pipelines", href: "#pipelines" },
      { label: "Preview Deploys", href: "#preview" },
      { label: "Observability", href: "#observability" },
      { label: "Secrets Vault", href: "#secrets" },
      { label: "Edge Cache", href: "#edge" },
      { label: "Changelog", href: "#changelog", badge: "New" },
    ],
  },
  {
    title: "Developers",
    links: [
      { label: "Documentation", href: "#docs" },
      { label: "API Reference", href: "#api" },
      { label: "CLI Guide", href: "#cli" },
      { label: "System Status", href: "#status" },
      { label: "Open Source", href: "#oss" },
      { label: "Community Forum", href: "#community" },
    ],
  },
  {
    title: "Company",
    links: [
      { label: "About Northloop", href: "#about" },
      { label: "Careers", href: "#careers", badge: "12 open" },
      { label: "Engineering Blog", href: "#blog" },
      { label: "Customers", href: "#customers" },
      { label: "Press Kit", href: "#press" },
      { label: "Contact Sales", href: "#contact" },
    ],
  },
  {
    title: "Legal",
    links: [
      { label: "Privacy Policy", href: "#privacy" },
      { label: "Terms of Service", href: "#terms" },
      { label: "Security & Trust", href: "#security" },
      { label: "Data Processing", href: "#dpa" },
      { label: "Cookie Settings", href: "#cookies" },
      { label: "Sub-processors", href: "#subprocessors" },
    ],
  },
];

const SOCIALS: { label: string; href: string; path: ReactNode }[] = [
  {
    label: "Northloop on GitHub",
    href: "https://github.com",
    path: (
      <path d="M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.1.68-.22.68-.49 0-.24-.01-.87-.01-1.71-2.78.62-3.37-1.37-3.37-1.37-.45-1.18-1.11-1.5-1.11-1.5-.91-.64.07-.62.07-.62 1 .07 1.53 1.06 1.53 1.06.89 1.56 2.34 1.11 2.91.85.09-.66.35-1.11.63-1.37-2.22-.26-4.55-1.14-4.55-5.06 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.71 0 0 .84-.28 2.75 1.05a9.36 9.36 0 0 1 5 0c1.91-1.33 2.75-1.05 2.75-1.05.55 1.41.2 2.45.1 2.71.64.72 1.03 1.63 1.03 2.75 0 3.93-2.34 4.8-4.57 5.05.36.32.68.94.68 1.9 0 1.37-.01 2.48-.01 2.82 0 .27.18.6.69.49A10.02 10.02 0 0 0 22 12.25C22 6.58 17.52 2 12 2Z" />
    ),
  },
  {
    label: "Northloop on X",
    href: "https://x.com",
    path: (
      <path d="M17.53 3H20.5l-6.5 7.43L21.75 21h-5.98l-4.69-6.13L5.7 21H2.72l6.96-7.95L2.25 3h6.13l4.24 5.6L17.53 3Zm-1.05 16.2h1.65L7.6 4.71H5.83l10.65 14.49Z" />
    ),
  },
  {
    label: "Northloop on LinkedIn",
    href: "https://linkedin.com",
    path: (
      <path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5ZM3 9h4v12H3V9Zm7 0h3.83v1.64h.05c.53-1 1.84-2.05 3.79-2.05 4.05 0 4.8 2.67 4.8 6.14V21H18.5v-5.62c0-1.34-.02-3.06-1.87-3.06-1.87 0-2.16 1.46-2.16 2.96V21H10.5V9Z" />
    ),
  },
  {
    label: "Northloop on YouTube",
    href: "https://youtube.com",
    path: (
      <path d="M23.5 6.9a3 3 0 0 0-2.11-2.12C19.5 4.25 12 4.25 12 4.25s-7.5 0-9.39.53A3 3 0 0 0 .5 6.9 31.3 31.3 0 0 0 0 12a31.3 31.3 0 0 0 .5 5.1 3 3 0 0 0 2.11 2.12c1.89.53 9.39.53 9.39.53s7.5 0 9.39-.53a3 3 0 0 0 2.11-2.12A31.3 31.3 0 0 0 24 12a31.3 31.3 0 0 0-.5-5.1ZM9.6 15.5v-7l6.2 3.5-6.2 3.5Z" />
    ),
  },
  {
    label: "Northloop on Discord",
    href: "https://discord.com",
    path: (
      <path d="M20.32 4.57A19.8 19.8 0 0 0 15.4 3.1a.07.07 0 0 0-.08.04c-.21.38-.45.87-.61 1.26a18.3 18.3 0 0 0-5.42 0c-.16-.4-.4-.88-.62-1.26a.08.08 0 0 0-.08-.04 19.7 19.7 0 0 0-4.9 1.47.07.07 0 0 0-.04.03C.6 9.24-.36 13.78.12 18.27a.08.08 0 0 0 .03.05 19.9 19.9 0 0 0 5.99 3 .08.08 0 0 0 .08-.03c.46-.63.87-1.29 1.22-1.99a.08.08 0 0 0-.04-.11c-.65-.24-1.27-.54-1.87-.88a.08.08 0 0 1-.01-.13l.37-.29a.07.07 0 0 1 .08-.01c3.92 1.79 8.16 1.79 12.03 0a.07.07 0 0 1 .08.01l.37.29a.08.08 0 0 1-.01.13c-.6.35-1.22.64-1.87.88a.08.08 0 0 0-.04.11c.36.7.77 1.36 1.22 1.99a.08.08 0 0 0 .08.03 19.8 19.8 0 0 0 6-3 .08.08 0 0 0 .03-.05c.57-5.2-.95-9.7-4.02-13.67a.06.06 0 0 0-.03-.03ZM8.02 15.53c-1.18 0-2.16-1.08-2.16-2.42s.96-2.42 2.16-2.42c1.21 0 2.18 1.09 2.16 2.42 0 1.34-.96 2.42-2.16 2.42Zm7.97 0c-1.18 0-2.16-1.08-2.16-2.42s.96-2.42 2.16-2.42c1.21 0 2.18 1.09 2.16 2.42 0 1.34-.95 2.42-2.16 2.42Z" />
    ),
  },
];

const REGIONS = [
  "United States (English)",
  "United Kingdom (English)",
  "Deutschland (Deutsch)",
  "France (Français)",
  "日本 (日本語)",
  "Brasil (Português)",
];

export default function FootxDark() {
  const reduce = useReducedMotion();
  const emailId = useId();
  const errorId = useId();
  const regionId = useId();
  const headingId = useId();
  const panelBaseId = useId();

  const [email, setEmail] = useState("");
  const [submittedEmail, setSubmittedEmail] = useState("");
  const [status, setStatus] = useState<SubscribeStatus>("idle");
  const [openCol, setOpenCol] = useState<number | null>(null);
  const [region, setRegion] = useState(REGIONS[0]);
  const [isDesktop, setIsDesktop] = useState(false);

  useEffect(() => {
    const mq = window.matchMedia("(min-width: 768px)");
    const sync = () => setIsDesktop(mq.matches);
    sync();
    mq.addEventListener("change", sync);
    return () => mq.removeEventListener("change", sync);
  }, []);

  function handleSubscribe(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const value = email.trim();
    const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
    if (!valid) {
      setStatus("error");
      return;
    }
    setSubmittedEmail(value);
    setStatus("success");
    setEmail("");
  }

  function resetSubscribe() {
    setStatus("idle");
    setSubmittedEmail("");
  }

  function scrollToTop() {
    window.scrollTo({ top: 0, behavior: reduce ? "auto" : "smooth" });
  }

  const rise = reduce
    ? {}
    : {
        initial: { opacity: 0, y: 18 },
        whileInView: { opacity: 1, y: 0 },
        viewport: { once: true, margin: "-80px" },
        transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] as const },
      };

  return (
    <section
      className="relative w-full overflow-hidden bg-gradient-to-b from-slate-900 to-slate-950 text-slate-400 dark:from-zinc-950 dark:to-black"
      aria-labelledby={headingId}
    >
      <style>{`
        @keyframes footx-sheen { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
        @keyframes footx-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .35; transform: scale(.7); } }
        @keyframes footx-pop { 0% { opacity: 0; transform: scale(0) rotate(-14deg); } 60% { transform: scale(1.15) rotate(2deg); } 100% { opacity: 1; transform: scale(1) rotate(0); } }
        .footx-sheen { animation: footx-sheen 7s linear infinite; }
        .footx-dot { animation: footx-pulse 2.4s ease-in-out infinite; }
        .footx-check { animation: footx-pop .5s cubic-bezier(.34,1.56,.64,1) both; }
        .footx-collapsible { transition: grid-template-rows .32s ease; }
        @media (prefers-reduced-motion: reduce) {
          .footx-sheen, .footx-dot, .footx-check { animation: none !important; }
          .footx-collapsible { transition: none !important; }
        }
      `}</style>

      {/* animated hairline */}
      <div
        aria-hidden="true"
        className="footx-sheen absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-indigo-400/70 to-transparent bg-[length:200%_100%]"
      />
      {/* ambient glow */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute -top-24 left-1/2 h-64 w-[42rem] max-w-full -translate-x-1/2 rounded-full bg-indigo-500/10 blur-3xl dark:bg-violet-500/10"
      />

      <h2 id={headingId} className="sr-only">
        Northloop site footer
      </h2>

      <div className="relative mx-auto max-w-7xl px-6 py-16 sm:px-8 sm:py-20">
        {/* Brand + newsletter */}
        <motion.div
          {...rise}
          className="grid gap-12 border-b border-white/10 pb-12 lg:grid-cols-[1.35fr_1fr] lg:gap-16"
        >
          <div className="max-w-md">
            <a
              href="#top"
              className="inline-flex items-center gap-2.5 rounded-lg text-slate-50 outline-none focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 dark:focus-visible:ring-offset-black"
              aria-label="Northloop home"
            >
              <svg
                viewBox="0 0 32 32"
                className="h-8 w-8"
                fill="none"
                aria-hidden="true"
              >
                <rect width="32" height="32" rx="9" className="fill-indigo-500" />
                <path
                  d="M9 22V10l7 8V10m0 12 7-8v8"
                  stroke="white"
                  strokeWidth="2.4"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                />
              </svg>
              <span className="text-lg font-semibold tracking-tight">
                Northloop
              </span>
            </a>

            <p className="mt-5 text-sm leading-relaxed text-slate-400">
              The deploy platform for teams that ship on Fridays. Build
              pipelines, preview every pull request, and watch it all run on the
              edge — without babysitting infrastructure.
            </p>

            <a
              href="#status"
              className="mt-6 inline-flex items-center gap-2 rounded-full border border-emerald-400/30 bg-emerald-400/10 px-3 py-1.5 text-xs font-medium text-emerald-300 outline-none transition-colors hover:border-emerald-400/50 hover:bg-emerald-400/15 focus-visible:ring-2 focus-visible:ring-emerald-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 dark:focus-visible:ring-offset-black"
            >
              <span className="footx-dot h-2 w-2 rounded-full bg-emerald-400" />
              All systems operational
            </a>

            <ul className="mt-7 flex flex-wrap gap-2.5">
              {SOCIALS.map((social) => (
                <li key={social.label}>
                  <a
                    href={social.href}
                    target="_blank"
                    rel="noopener"
                    aria-label={social.label}
                    className="group inline-flex h-10 w-10 items-center justify-center rounded-xl border border-white/10 bg-white/5 text-slate-400 outline-none transition-colors hover:border-white/20 hover:bg-white/10 hover:text-slate-100 focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 dark:focus-visible:ring-offset-black"
                  >
                    <svg
                      viewBox="0 0 24 24"
                      className="h-5 w-5 fill-current"
                      aria-hidden="true"
                    >
                      {social.path}
                    </svg>
                  </a>
                </li>
              ))}
            </ul>
          </div>

          {/* Newsletter */}
          <div className="lg:pl-8">
            <h3 className="text-sm font-semibold text-slate-100">
              Ship Notes — monthly
            </h3>
            <p className="mt-2 text-sm text-slate-400">
              One email a month: new releases, edge benchmarks, and a build
              teardown from a real customer. No fluff, unsubscribe anytime.
            </p>

            {status === "success" ? (
              <div
                role="status"
                className="mt-5 flex items-start gap-3 rounded-2xl border border-emerald-400/25 bg-emerald-400/10 p-4"
              >
                <span className="footx-check mt-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-emerald-400 text-slate-950">
                  <svg
                    viewBox="0 0 24 24"
                    className="h-4 w-4"
                    fill="none"
                    aria-hidden="true"
                  >
                    <path
                      d="m5 12 4.5 4.5L19 7"
                      stroke="currentColor"
                      strokeWidth="2.5"
                      strokeLinecap="round"
                      strokeLinejoin="round"
                    />
                  </svg>
                </span>
                <div className="min-w-0">
                  <p className="text-sm font-medium text-emerald-200">
                    You&apos;re on the list.
                  </p>
                  <p className="mt-1 break-words text-sm text-emerald-300/80">
                    The next issue is headed to {submittedEmail}.
                  </p>
                  <button
                    type="button"
                    onClick={resetSubscribe}
                    className="mt-2 rounded text-sm font-medium text-emerald-200 underline decoration-emerald-400/40 underline-offset-4 outline-none transition-colors hover:text-white focus-visible:ring-2 focus-visible:ring-emerald-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 dark:focus-visible:ring-offset-black"
                  >
                    Use a different email
                  </button>
                </div>
              </div>
            ) : (
              <form onSubmit={handleSubscribe} noValidate className="mt-5">
                <label
                  htmlFor={emailId}
                  className="block text-xs font-medium text-slate-300"
                >
                  Work email
                </label>
                <div className="mt-2 flex flex-col gap-2.5 sm:flex-row">
                  <input
                    id={emailId}
                    type="email"
                    name="email"
                    inputMode="email"
                    autoComplete="email"
                    placeholder="you@company.com"
                    value={email}
                    onChange={(event) => {
                      setEmail(event.target.value);
                      if (status === "error") setStatus("idle");
                    }}
                    aria-invalid={status === "error"}
                    aria-describedby={status === "error" ? errorId : undefined}
                    className="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-slate-100 placeholder:text-slate-500 outline-none transition-colors focus-visible:border-indigo-400 focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 aria-[invalid=true]:border-rose-400/70 aria-[invalid=true]:ring-1 aria-[invalid=true]:ring-rose-400/40 dark:focus-visible:ring-offset-black"
                  />
                  <button
                    type="submit"
                    className="shrink-0 rounded-xl bg-indigo-500 px-5 py-2.5 text-sm font-semibold text-white outline-none transition-colors hover:bg-indigo-400 focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 dark:focus-visible:ring-offset-black"
                  >
                    Subscribe
                  </button>
                </div>
                <p
                  id={errorId}
                  role="alert"
                  aria-live="polite"
                  className="mt-2 min-h-[1.25rem] text-xs text-rose-400"
                >
                  {status === "error"
                    ? "Please enter a valid email address."
                    : ""}
                </p>
              </form>
            )}
          </div>
        </motion.div>

        {/* Link columns */}
        <motion.nav
          {...rise}
          aria-label="Footer"
          className="grid grid-cols-1 gap-x-8 py-4 sm:grid-cols-2 md:grid-cols-4 md:py-10"
        >
          {COLUMNS.map((column, index) => {
            const expanded = isDesktop || openCol === index;
            const panelId = `${panelBaseId}-${index}`;
            return (
              <div
                key={column.title}
                className="border-b border-white/10 py-2 md:border-0 md:py-0"
              >
                <h3>
                  <button
                    type="button"
                    onClick={() =>
                      setOpenCol((current) =>
                        current === index ? null : index,
                      )
                    }
                    aria-expanded={expanded}
                    aria-controls={panelId}
                    className="flex w-full items-center justify-between gap-2 rounded-md py-3 text-left text-sm font-semibold text-slate-100 outline-none transition-colors hover:text-white focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 md:cursor-default md:py-0 md:pb-4 dark:focus-visible:ring-offset-black"
                  >
                    {column.title}
                    <svg
                      viewBox="0 0 24 24"
                      className={`h-4 w-4 text-slate-400 transition-transform duration-300 md:hidden ${
                        expanded ? "rotate-180" : ""
                      }`}
                      fill="none"
                      aria-hidden="true"
                    >
                      <path
                        d="m6 9 6 6 6-6"
                        stroke="currentColor"
                        strokeWidth="2"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                      />
                    </svg>
                  </button>
                </h3>
                <div
                  id={panelId}
                  className={`footx-collapsible grid md:grid-rows-[1fr] ${
                    expanded ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
                  }`}
                >
                  <div className="overflow-hidden">
                    <ul className="space-y-3 pb-4 md:pb-0">
                      {column.links.map((link) => (
                        <li key={link.label}>
                          <a
                            href={link.href}
                            className="group inline-flex items-center gap-2 rounded text-sm text-slate-400 outline-none transition-colors hover:text-slate-100 focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 dark:focus-visible:ring-offset-black"
                          >
                            <span className="underline decoration-transparent decoration-1 underline-offset-4 transition-colors group-hover:decoration-indigo-400">
                              {link.label}
                            </span>
                            {link.badge ? (
                              <span className="rounded-full border border-indigo-400/30 bg-indigo-400/10 px-1.5 py-0.5 text-[10px] font-medium leading-none text-indigo-300">
                                {link.badge}
                              </span>
                            ) : null}
                          </a>
                        </li>
                      ))}
                    </ul>
                  </div>
                </div>
              </div>
            );
          })}
        </motion.nav>

        {/* Bottom bar */}
        <div className="mt-6 flex flex-col gap-6 border-t border-white/10 pt-8 md:flex-row md:items-center md:justify-between">
          <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:gap-6">
            <p className="text-xs text-slate-500">
              © {new Date().getFullYear()} Northloop, Inc. All rights reserved.
            </p>
            <div className="flex items-center gap-2">
              <label htmlFor={regionId} className="sr-only">
                Region and language
              </label>
              <svg
                viewBox="0 0 24 24"
                className="h-4 w-4 text-slate-500"
                fill="none"
                aria-hidden="true"
              >
                <circle
                  cx="12"
                  cy="12"
                  r="9"
                  stroke="currentColor"
                  strokeWidth="1.6"
                />
                <path
                  d="M3 12h18M12 3c2.5 2.5 2.5 15 0 18M12 3c-2.5 2.5-2.5 15 0 18"
                  stroke="currentColor"
                  strokeWidth="1.6"
                  strokeLinecap="round"
                />
              </svg>
              <select
                id={regionId}
                value={region}
                onChange={(event) => setRegion(event.target.value)}
                className="rounded-lg border border-white/10 bg-white/5 py-1.5 pl-2.5 pr-8 text-xs text-slate-300 outline-none transition-colors hover:border-white/20 focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 dark:focus-visible:ring-offset-black"
              >
                {REGIONS.map((option) => (
                  <option
                    key={option}
                    value={option}
                    className="bg-slate-900 text-slate-100"
                  >
                    {option}
                  </option>
                ))}
              </select>
            </div>
          </div>

          <button
            type="button"
            onClick={scrollToTop}
            className="inline-flex items-center gap-2 self-start rounded-full border border-white/10 bg-white/5 px-4 py-2 text-xs font-medium text-slate-300 outline-none transition-colors hover:border-white/20 hover:text-slate-100 focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 md:self-auto dark:focus-visible:ring-offset-black"
          >
            Back to top
            <svg
              viewBox="0 0 24 24"
              className="h-3.5 w-3.5"
              fill="none"
              aria-hidden="true"
            >
              <path
                d="M12 19V5m0 0-6 6m6-6 6 6"
                stroke="currentColor"
                strokeWidth="2"
                strokeLinecap="round"
                strokeLinejoin="round"
              />
            </svg>
          </button>
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

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 quote

Similar components

Browse all →