Skip to content
Layout

Layout

SiteFooter

Wrapping site footer with a haloed brand dot, mono headings and a legal row.

npx bezel-add add site-footer

About

A responsive site footer for React with a brand name beside a haloed accent dot, an optional tagline, link columns with mono headings, and a bottom row for copyright and an optional "Published by" credit. It wraps with flexbox rather than breakpoints, so one column or five both lay out cleanly, and colours adapt to dark mode through light-dark().

Key props: brandName (text beside the dot), tagline (short line under the brand), columns (headings with link lists), copyright (text in the bottom row)

packages/ui/src/layout/SiteFooter.tsx · 167 lines

import type { CSSProperties } from "react";

type FooterColumn = {
  heading: string;
  links: { label: string; href: string }[];
};

type Props = {
  brandName?: string;
  tagline?: string;
  columns?: FooterColumn[];
  copyright?: string;
  publishedBy?: { label: string; href: string };
};

/**
 * Pairs are picked with light-dark() from tokens.css, including the focus
 * ring, which has to change colour with the ground or it stops being visible.
 * A host that declares `color-scheme: dark` needs no prop.
 */
const theme = {
  "--sf-ink": "light-dark(var(--bz-ink, #0a0a0a), var(--bz-void-ink, #ffffff))",
  "--sf-muted": "light-dark(var(--bz-ink-muted, #4a4a4c), rgba(255, 255, 255, 0.78))",
  "--sf-subtle": "light-dark(var(--bz-ink-subtle, #6b6b70), rgba(255, 255, 255, 0.66))",
  "--sf-line": "light-dark(rgba(10, 10, 10, 0.1), rgba(255, 255, 255, 0.13))",
  "--sf-accent": "light-dark(var(--bz-emerald, #047857), var(--bz-emerald-on-void, #34d399))",
  "--sf-halo": "light-dark(rgba(4, 120, 87, 0.16), rgba(52, 211, 153, 0.22))",
  "--sf-ring": "light-dark(var(--bz-focus-ring, #912c22), var(--bz-focus-ring-void, #ffffff))",
} as CSSProperties;

/* Colour stays in classes, not the style attribute, or :hover could never win. */
const linkClass =
  "no-underline transition-colors hover:[color:var(--sf-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:[outline-color:var(--sf-ring)]";

export function SiteFooter({
  brandName = "YASH",
  tagline = "",
  columns = [],
  copyright = `© ${new Date().getFullYear()} YASH. All rights reserved.`,
  publishedBy,
}: Props) {
  return (
    <footer
      style={{
        ...theme,
        borderTop: "1px solid var(--sf-line)",
        marginTop: "2.5rem",
        padding: "clamp(2.5rem, 6vw, 4rem) clamp(1rem, 4vw, 1.5rem) 1.75rem",
      }}
    >
      {/* Two levels of wrapping rather than a fixed track count: the brand and
          the link block break apart first, then the columns break among
          themselves. One column and five both land without a breakpoint.
          The gaps are fixed rather than viewport-relative, so where the
          footer breaks depends on the footer's own width and not on the
          window it is being scaled inside. */}
      <div
        className="mx-auto flex flex-wrap"
        style={{ columnGap: "3rem", maxWidth: "1200px", rowGap: "2.5rem" }}
      >
        <div style={{ flex: "1 1 18rem", minWidth: 0 }}>
          <div
            className="flex items-center"
            style={{
              color: "var(--sf-ink)",
              fontSize: "0.9375rem",
              fontWeight: "var(--bz-weight-semibold, 600)",
              gap: "0.75rem",
              letterSpacing: "0.08em",
            }}
          >
            <span
              aria-hidden
              className="shrink-0 rounded-full"
              style={{
                background: "var(--sf-accent)",
                boxShadow: "0 0 0 4px var(--sf-halo)",
                height: "0.5rem",
                width: "0.5rem",
              }}
            />
            {brandName}
          </div>
          {tagline && (
            <p
              className="mb-0"
              style={{
                color: "var(--sf-muted)",
                fontSize: "var(--bz-text-sm, 0.875rem)",
                lineHeight: "var(--bz-leading-normal, 1.55)",
                marginTop: "0.875rem",
                maxWidth: "34ch",
              }}
            >
              {tagline}
            </p>
          )}
        </div>

        {columns.length > 0 && (
          <div
            className="flex flex-wrap"
            style={{ columnGap: "2rem", flex: "2 1 22rem", rowGap: "2rem" }}
          >
            {columns.map((col) => (
              <div key={col.heading} style={{ flex: "1 1 7.5rem", minWidth: "7rem" }}>
                <h4
                  className="m-0 font-mono uppercase"
                  style={{
                    color: "var(--sf-accent)",
                    fontSize: "var(--bz-text-2xs, 0.6875rem)",
                    fontWeight: "var(--bz-weight-normal, 400)",
                    letterSpacing: "0.24em",
                    lineHeight: 1,
                  }}
                >
                  {col.heading}
                </h4>
                <ul className="m-0 list-none p-0" style={{ marginTop: "0.5rem" }}>
                  {col.links.map((l) => (
                    <li key={l.href}>
                      <a
                        href={l.href}
                        className={`${linkClass} [color:var(--sf-ink)]`}
                        style={{
                          display: "inline-block",
                          fontSize: "var(--bz-text-sm, 0.875rem)",
                          lineHeight: 1.5,
                          padding: "0.4375rem 0",
                        }}
                      >
                        {l.label}
                      </a>
                    </li>
                  ))}
                </ul>
              </div>
            ))}
          </div>
        )}
      </div>

      <div
        className="mx-auto flex flex-wrap items-center justify-between"
        style={{
          borderTop: "1px solid var(--sf-line)",
          color: "var(--sf-subtle)",
          fontSize: "var(--bz-text-xs, 0.75rem)",
          gap: "0.75rem",
          marginTop: "2.5rem",
          maxWidth: "1200px",
          paddingTop: "1.375rem",
        }}
      >
        <span>{copyright}</span>
        {publishedBy && (
          <span>
            Published by{" "}
            <a href={publishedBy.href} className={`${linkClass} [color:var(--sf-muted)]`}>
              {publishedBy.label}
            </a>
          </span>
        )}
      </div>
    </footer>
  );
}