Web InnoventixFreeCode

Minimal Footer

Original · free

minimal centred footer

byWeb InnoventixReact + Tailwind
footxminimalfooters
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-minimal.json
footx-minimal.tsx
"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>&copy; {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

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 →