Skip to content

Insignia Corner Nav

A heritage header: the house's insignia worn small in a gold roundel in one corner, sections centred in spaced capitals with drawn underlines, and a Didone menu sheet on phones.

FreeNeoclassicaleditorialminimal
Category
Components
Added
2026-09-26
Deps
none
Updated
2026-09-26
Preview

Auctions

The Autumn Season

Props

Sign-in link
No runtime dependencies
"use client";

import { useEffect, useId, useRef, useState } from "react";

/**
 * InsigniaCornerNav — a heritage header: the house's insignia worn small in
 * one corner, the sections centred in spaced capitals, and a quiet account
 * link in the other corner.
 *
 * Every link underlines by drawing a one-pixel rule outward from its centre
 * (600ms, no overshoot) on hover or keyboard focus; the current page keeps
 * its rule. The insignia is a roundel drawn in one gold hairline around the
 * house's initials, the only metal on the bar. A full-width hairline runs
 * under it.
 *
 * Below 768px of container width the links fold into a Menu button. The menu
 * opens as a sheet under the bar with the sections in a Didone, one per
 * line between rules; it takes focus, closes on Escape, on a press outside
 * or on choosing a link, and gives focus back to the button.
 *
 * Type reads var(--font-didone) and falls back to system Didones, then
 * Georgia. Needs Tailwind v4, or v3.4+ with @tailwindcss/container-queries.
 * No dependencies beyond React. Without scripting the menu sheet is simply
 * shown under the bar on narrow screens.
 */

const DIDONE = 'var(--font-didone, "Bodoni Moda", "Didot", "Bodoni 72", "Bodoni MT", Georgia, serif)';

const TONES = {
  ivory: { ground: "#f4f1ea", ink: "#14120f", muted: "rgba(20,18,15,0.62)", rule: "rgba(20,18,15,0.16)" },
  charcoal: { ground: "#14120f", ink: "#f4f1ea", muted: "rgba(244,241,234,0.62)", rule: "rgba(244,241,234,0.16)" },
} as const;

const HEX = /^#[0-9a-fA-F]{6}$/;

/** Links: strip tabs and newlines, refuse backslashes, allow http(s), mailto, tel and same-site paths. */
function safeHref(raw: string) {
  const v = raw.replace(/[\t\n\r]/g, "").trim();
  if (!v || v.includes("\\")) return "#";
  if (/^(https?:|mailto:|tel:)/i.test(v)) return v;
  if (/^[/#?]/.test(v) && !/^\/\//.test(v)) return v;
  return "#";
}

export type InsigniaLink = { label: string; href: string; current?: boolean };

export type InsigniaCornerNavProps = {
  /** The house's name, set beside the insignia on wide screens. */
  name: string;
  /** One or two initials for the roundel; the second is set after an italic ampersand. */
  initials: [string] | [string, string];
  /** Where the insignia links to. */
  homeHref?: string;
  links: InsigniaLink[];
  /** A quiet link in the far corner: sign in, book, contact. */
  aside?: { label: string; href: string };
  tone?: "ivory" | "charcoal";
  /** The insignia's metal, as #rrggbb. */
  metal?: string;
  className?: string;
};

// Static rules only: without scripting, narrow screens show the sheet in the
// flow and hide the Menu button (unlayered, so it outranks the utilities).
const CSS =
  "@media (scripting: none) { @container (max-width: 47.99rem) { [data-icn-sheet] { display: block; position: static; } [data-icn-menu] { display: none; } } }";

const RULE =
  "pointer-events-none absolute inset-x-0 bottom-[7px] h-px origin-center transition-transform duration-[600ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none";

export function InsigniaCornerNav({
  name,
  initials,
  homeHref = "/",
  links,
  aside,
  tone = "ivory",
  metal = "#b8964f",
  className = "",
}: InsigniaCornerNavProps) {
  const t = tone === "charcoal" ? TONES.charcoal : TONES.ivory;
  const gold = HEX.test(metal) ? metal : "#b8964f";
  const menuId = "icn" + useId().replace(/[^a-zA-Z0-9]/g, "");
  const [open, setOpen] = useState(false);
  const rootRef = useRef<HTMLElement>(null);
  const buttonRef = useRef<HTMLButtonElement>(null);
  const sheetRef = useRef<HTMLDivElement>(null);
  // Set when the menu closes by a choice that should hand focus back.
  const refocus = useRef(false);

  useEffect(() => {
    if (!open) {
      if (refocus.current) buttonRef.current?.focus();
      refocus.current = false;
      return;
    }
    sheetRef.current?.querySelector<HTMLElement>("a")?.focus();
    const root = rootRef.current;
    // Tabbing out of the open sheet closes it rather than leaving it over the page.
    function onOut(e: FocusEvent) {
      const to = e.relatedTarget as Node | null;
      if (to && root && !root.contains(to)) setOpen(false);
    }
    // Widening past the breakpoint hides the sheet and the button: close too.
    const ro = new ResizeObserver(() => {
      if (root && root.offsetWidth >= 768) setOpen(false);
    });
    if (root) {
      root.addEventListener("focusout", onOut);
      ro.observe(root);
    }
    function onKey(e: KeyboardEvent) {
      if (e.key === "Escape") {
        refocus.current = true;
        setOpen(false);
      }
    }
    function onDown(e: PointerEvent) {
      if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
    }
    document.addEventListener("keydown", onKey);
    document.addEventListener("pointerdown", onDown);
    return () => {
      document.removeEventListener("keydown", onKey);
      document.removeEventListener("pointerdown", onDown);
      root?.removeEventListener("focusout", onOut);
      ro.disconnect();
    };
  }, [open]);

  const [first, second] = initials;

  const link = (l: InsigniaLink, i: number) => (
    <li key={i}>
      <a
        href={safeHref(l.href)}
        aria-current={l.current ? "page" : undefined}
        className="group relative inline-flex h-10 items-center text-[11px] uppercase tracking-[0.22em] outline-offset-4 focus-visible:outline focus-visible:outline-1"
        style={{ fontFamily: DIDONE, color: l.current ? t.ink : t.muted, outlineColor: t.ink }}
      >
        <span className="transition-colors duration-300 group-hover:text-[color:var(--icn-ink)] group-focus-visible:text-[color:var(--icn-ink)]" style={{ ["--icn-ink" as string]: t.ink }}>
          {l.label}
        </span>
        <span
          aria-hidden
          className={RULE + (l.current ? " scale-x-100" : " scale-x-0 group-hover:scale-x-100 group-focus-visible:scale-x-100")}
          style={{ background: t.ink }}
        />
      </a>
    </li>
  );

  return (
    <nav ref={rootRef} aria-label="Main" className={"@container relative z-20 " + className} style={{ background: t.ground, color: t.ink }}>
      <style>{CSS}</style>
      <div className="mx-auto grid h-20 w-full max-w-[1280px] grid-cols-[1fr_auto] items-center gap-6 px-6 @3xl:grid-cols-[1fr_auto_1fr] @3xl:px-24">
        <a href={safeHref(homeHref)} className="flex items-center gap-4 justify-self-start outline-offset-4 focus-visible:outline focus-visible:outline-1" style={{ outlineColor: t.ink }}>
          <svg width="40" height="40" viewBox="0 0 40 40" aria-hidden className="shrink-0 overflow-visible">
            <circle cx="20" cy="20" r="19.5" fill="none" stroke={gold} strokeWidth="1" />
            <circle cx="20" cy="20" r="16.5" fill="none" stroke={gold} strokeWidth="0.5" strokeOpacity={0.55} />
            <text x="20" y="24.2" textAnchor="middle" fontSize={second ? 11.5 : 14} fill={t.ink} style={{ fontFamily: DIDONE, letterSpacing: "0.04em" }}>
              {first}
              {second && (
                <>
                  <tspan fontStyle="italic" fontSize="10" dx="0.5">
                    &amp;
                  </tspan>
                  <tspan dx="0.5">{second}</tspan>
                </>
              )}
            </text>
          </svg>
          <span className="hidden whitespace-nowrap text-[12px] uppercase tracking-[0.26em] @4xl:inline" style={{ fontFamily: DIDONE }}>
            {name}
          </span>
          <span className="sr-only @4xl:hidden">{name}</span>
        </a>

        <ul role="list" className="hidden items-center gap-9 @3xl:flex">
          {links.map(link)}
        </ul>

        <div className="flex items-center justify-self-end gap-6">
          {aside && (
            <a
              href={safeHref(aside.href)}
              className="group relative hidden h-10 items-center text-[11px] uppercase tracking-[0.22em] outline-offset-4 focus-visible:outline focus-visible:outline-1 @3xl:inline-flex"
              style={{ fontFamily: DIDONE, outlineColor: t.ink }}
            >
              {aside.label}
              <span aria-hidden className={RULE} style={{ background: t.rule }} />
              <span aria-hidden className={RULE + " scale-x-0 group-hover:scale-x-100 group-focus-visible:scale-x-100"} style={{ background: t.ink }} />
            </a>
          )}
          <button
            ref={buttonRef}
            data-icn-menu=""
            type="button"
            aria-expanded={open}
            aria-controls={menuId}
            onClick={() => setOpen((o) => !o)}
            className="inline-flex h-10 items-center gap-3 text-[11px] uppercase tracking-[0.22em] outline-offset-4 focus-visible:outline focus-visible:outline-1 @3xl:hidden"
            style={{ fontFamily: DIDONE, outlineColor: t.ink }}
          >
            {open ? "Close" : "Menu"}
            <span aria-hidden className="relative block h-2 w-5">
              <span
                className="absolute inset-x-0 top-0 h-px transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"
                style={{ background: t.ink, transform: open ? "translateY(3.5px) rotate(20deg)" : "none" }}
              />
              <span
                className="absolute inset-x-0 bottom-0 h-px transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"
                style={{ background: t.ink, transform: open ? "translateY(-3.5px) rotate(-20deg)" : "none" }}
              />
            </span>
          </button>
        </div>
      </div>
      <div aria-hidden className="h-px w-full" style={{ background: t.rule }} />

      <div
        ref={sheetRef}
        id={menuId}
        data-icn-sheet=""
        className={"absolute inset-x-0 top-full @3xl:hidden " + (open ? "block" : "hidden")}
        style={{ background: t.ground, borderBottom: "1px solid " + t.rule }}
      >
        <ul role="list" className="px-6 pb-8 pt-2">
          {links.map((l, i) => (
            <li key={i} style={{ borderBottom: "1px solid " + t.rule }}>
              <a
                href={safeHref(l.href)}
                aria-current={l.current ? "page" : undefined}
                onClick={() => setOpen(false)}
                className="flex items-baseline justify-between py-4 text-[28px] leading-tight outline-offset-2 focus-visible:outline focus-visible:outline-1"
                style={{ fontFamily: DIDONE, outlineColor: t.ink, fontStyle: l.current ? "italic" : undefined }}
              >
                {l.label}
                <span className="text-[10px] uppercase tracking-[0.22em]" style={{ color: t.muted }}>
                  {String(i + 1).padStart(2, "0")}
                </span>
              </a>
            </li>
          ))}
          {aside && (
            <li className="pt-6">
              <a
                href={safeHref(aside.href)}
                onClick={() => setOpen(false)}
                className="inline-flex h-10 items-center text-[11px] uppercase tracking-[0.22em] outline-offset-4 focus-visible:outline focus-visible:outline-1"
                style={{ fontFamily: DIDONE, color: t.muted, outlineColor: t.ink }}
              >
                {aside.label}
              </a>
            </li>
          )}
        </ul>
      </div>
    </nav>
  );
}
Notes

About this component

Heritage houses wear their logo small, like an insignia on a cuff, and let the sections carry the header. This one puts the house's initials in a gold hairline roundel in one corner, with an italic ampersand between them, and its name in spaced capitals beside it on wide screens. The sections sit exactly centred in small spaced capitals, and each underlines by drawing a one-pixel rule outward from its centre when you point at it or tab to it; the current page keeps its rule. A quiet account link balances the other corner, and a full-width hairline runs underneath. On a phone the sections fold into a Menu button, and the menu opens as a sheet under the bar with each section set large in the Didone, numbered, between rules. It takes focus when it opens, closes on Escape, on a press outside or when you choose a link, and hands focus back to the button. The roundel is the only metal on the bar.

Curator’s note

Part of the Neoclassical kit, drop 3, phase 8: the navigation. Free: one header with a small open and close state for the phone menu.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

New

A heritage button in spaced Didone capitals: on hover a gold hairline engraves itself under the label from the centre out. No fill, no scale, no spring.

buttonneoclassicalserif
New

Site navigation as one row of bento tiles sharing 1px hairlines, with a pointer light that shows through them and menus that open as panels of tiles.

navigationnavbarmega menu
Featured
NewPro

A centred Didone headline under a segmental arch drawn in one gold hairline: it rises from both bases, meets at a keystone, and catches a raking light under the pointer.

heroneoclassicaldidone