← All components

HeadingAnchor

A section number in a heading's gutter that becomes a # and copies the link.

#Where churn comes from

Point at the heading, then press the mark to copy its link.

#The annual offer

Point at the heading, then press the mark to copy its link.

The idea

One slot doing two jobs, because a heading only has one margin. At rest it shows the section number; pointed at, it becomes a # that copies the deep link and confirms with a check. It is a real link, so it survives copy link address and shows itself on keyboard focus rather than being an invisible tab stop — and the URL changes even when the clipboard refuses, since the address bar is the fallback.

Install

terminal1 line
npm i mihirmodi-ui motion

The tokens, the theme script and the routing provider are a one-time job — see Setup. Or skip the package entirely and take the source at the foot of this page.

Usage

HeadingAnchor — usage8 lines
import { HeadingAnchor } from "mihirmodi-ui";

// The heading carries the group and the positioning context; the anchor
// hangs in its left margin. Leave it out where there is no margin.
<h2 id="annual-offer" className="group/heading relative scroll-mt-28">
  <HeadingAnchor id="annual-offer" label="The annual offer" index="02" />
  The annual offer
</h2>

Props

PropNotes
idstringThe heading's own id — what the link points at.
labelstringThe heading's text, for the name a screen reader hears.
indexstringA section number like "04", shown at rest.
classNamestringPlacement in the gutter. The heading needs `group/heading relative`.

Source

Exactly what the package ships — this is read out of the source at build time, not a copy. Take it and adapt it if you would rather not add a dependency.

packages/ui/src/components/HeadingAnchor.tsx81 lines
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { CheckIcon } from "../lib/glyph.js";

export type HeadingAnchorProps = {
  /** The heading's id — what the link points at. */
  id: string;
  /** The heading's text, for the name a screen reader hears. */
  label: string;
  /** A section number like "04", shown at rest on pages that number their sections. */
  index?: string;
  /** Placement in the heading's gutter. The default hangs it 40px left of the heading. */
  className?: string;
};

const CONFIRM_MS = 1600;

/**
 * The gutter mark on a heading: which section this is, and a way to link to it.
 *
 * One slot doing two jobs, because a heading only has one margin. At rest it
 * shows the section number, or nothing; pointed at, it becomes a `#` that copies
 * the deep link and confirms with a check. The two never show at once, so the
 * gutter stays one mark wide however it is used.
 *
 * It is a real `<a href="#id">`, so it works with no JavaScript, survives
 * "copy link address", and shows itself on keyboard focus rather than being an
 * invisible tab stop. The URL changes even if the clipboard refuses, since the
 * address bar is the fallback — and no check appears for a copy that failed.
 *
 * The heading needs `group/heading relative`, and a margin for the mark to hang
 * in; below that width, leave the anchor out rather than cover the text.
 */
export function HeadingAnchor({ id, label, index, className = "absolute -left-10 top-[5px]" }: HeadingAnchorProps) {
  const [copied, setCopied] = useState(false);
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => () => void (timer.current && clearTimeout(timer.current)), []);

  const copy = useCallback(
    (e: React.MouseEvent) => {
      e.preventDefault();
      window.history.replaceState(null, "", `#${id}`);
      navigator.clipboard?.writeText(`${location.origin}${location.pathname}#${id}`).then(
        () => {
          setCopied(true);
          if (timer.current) clearTimeout(timer.current);
          timer.current = setTimeout(() => setCopied(false), CONFIRM_MS);
        },
        () => {}
      );
    },
    [id]
  );

  return (
    <span className={`block font-mono text-[11px] font-normal leading-none text-faint ${className}`}>
      {index && (
        <span
          aria-hidden
          className={`transition-opacity duration-150 ease-geist group-hover/heading:opacity-0 group-focus-within/heading:opacity-0 ${copied ? "opacity-0" : ""}`}
        >
          {index}
        </span>
      )}
      <a
        href={`#${id}`}
        onClick={copy}
        aria-label={copied ? `Link to ${label} copied` : `Copy link to ${label}`}
        // Pulled out and padded back in: the glyph stays on the number's
        // baseline while the target is 27px square.
        className={`absolute -left-2 -top-2 p-2 leading-none transition-opacity duration-150 ease-geist hover:text-gray-1000 focus-visible:opacity-100 group-hover/heading:opacity-100 motion-reduce:transition-none ${
          copied ? "text-gray-1000 opacity-100" : "opacity-0"
        }`}
      >
        {copied ? <CheckIcon className="h-3 w-3" /> : "#"}
      </a>
    </span>
  );
}