Skip to content

Add Anchor Links to Headings in Starlight

3 min read

This guide explains how to add anchor links to headings in your Starlight project, making it easier for users to navigate and share specific sections.

  1. Install the necessary dependencies using your package manager:

    Terminal window
    npm add rehype-autolink-headings rehype-slug hast-util-to-string hastscript html-escaper

    You may also need to add TypeScript definitions:

    Terminal window
    npm add --dev @types/hast @types/html-escaper
  2. Create a new file named rehype-autolink.ts in the plugins directory:

    plugins/rehype-autolink.ts
    // For more details, see: https://github.com/biomejs/website/blob/main/plugins/rehype-autolink.ts
    import type { RehypePlugins } from "astro";
    import type { Element } from "hast";
    import { toString as hastToString } from "hast-util-to-string";
    import { h } from "hastscript";
    import { escape as htmlEscape } from "html-escaper";
    import rehypeAutolinkHeadings from "rehype-autolink-headings";
    const anchorLinkIcon = h(
    "span",
    { ariaHidden: "true", class: "anchor-icon" },
    h(
    "svg",
    { width: 16, height: 16, viewBox: "0 0 24 24" },
    h("path", {
    fill: "currentcolor",
    d: "m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z",
    }),
    ),
    );
    const anchorLinkSRLabel = (text: string) =>
    h("span", { "is:raw": true, class: "sr-only" }, `Section titled ${htmlEscape(text)}`);
    const autolinkConfig = {
    properties: { class: "anchor-link" },
    behavior: "after",
    group: ({ tagName }: Partial<Element>) =>
    h("div", {
    tabIndex: -1,
    class: `heading-wrapper level-${tagName}`,
    }),
    content: (heading: Element) => [
    anchorLinkIcon,
    anchorLinkSRLabel(hastToString(heading)),
    ],
    };
    export const rehypeAutolink = (): RehypePlugins => [
    [rehypeAutolinkHeadings, autolinkConfig],
    ];
  3. Import and configure the plugin in astro.config.mjs:

    astro.config.mjs
    import { rehypeAutolink } from "./plugins/rehype-autolink";
    import rehypeSlug from "rehype-slug";
    export default defineConfig({
    markdown: {
    rehypePlugins: [rehypeSlug, ...rehypeAutolink()],
    },
    integrations: [
    starlight({
    customCss: ["./src/styles/headings.css"],
    }),
    ],
    });
  4. Create a headings.css file in src/styles/ and add the following styles:

    src/styles/headings.css
    .sl-markdown-content .heading-wrapper {
    --icon-size: 0.75em;
    --icon-spacing: 0.25em;
    line-height: var(--sl-line-height-headings);
    }
    .sl-markdown-content .level-h2 {
    font-size: var(--sl-text-h2);
    }
    .sl-markdown-content .level-h3 {
    font-size: var(--sl-text-h3);
    }
    .sl-markdown-content .level-h4 {
    font-size: var(--sl-text-h4);
    }
    .sl-markdown-content .level-h5 {
    font-size: var(--sl-text-h5);
    }
    .sl-markdown-content .anchor-link {
    margin-inline-start: calc(-1 * (var(--icon-size)));
    color: var(--sl-color-gray-3);
    opacity: 0;
    transition: opacity 0.2s ease;
    }
    .sl-markdown-content .heading-wrapper:hover > .anchor-link,
    .sl-markdown-content .anchor-link:focus {
    opacity: 1;
    }
    @media (min-width: 95em) {
    .sl-markdown-content .heading-wrapper {
    display: flex;
    flex-direction: row-reverse;
    justify-content: flex-end;
    gap: var(--icon-spacing);
    margin-inline-start: calc(-1 * (var(--icon-size) + var(--icon-spacing)));
    }
    }
  1. Bring back heading anchor links