mrkeyoor.com_
Sat 08 Aug 22:53 UTC
npmUtilsupdated 08 Aug 2026

markdown-it-anchor

markdown-it-anchor is a markdown-it plugin that adds stable id attributes to Markdown headings and can insert configurable permalink links. It selects heading levels, creates unique slugs, reuses IDs set by earlier plugins, adds tabindex=-1 by default for focus after fragment navigation, and exposes several permalink layouts with different accessibility tradeoffs. It also supports custom slugging, state-aware slugs, callbacks for collecting a heading index, and both CommonJS and ESM builds with TypeScript declarations.

Verdict

markdown-it-anchor is the sensible markdown-it choice when heading fragments are part of the product, especially if you use its accessible permalink renderers. Decide slug and accessibility policies up front; defaults are intentionally minimal, and changing them later breaks URLs.

API stability4/5The core plugin contract has stayed compact: register it with markdown-it, configure level and slug creation, and optionally mutate heading tokens through a permalink renderer or callback. Version 8 intentionally replaced poor legacy permalink defaults with named accessible renderers, while keeping deprecated options behind warnings. Version 9 adds typed state-aware customization and preserves CommonJS compatibility. The remaining risk is that slug output and token mutation are user-visible URL and HTML contracts, so even justified option changes can be breaking.
Docs5/5The README covers every top-level option, default slug behavior, state-aware slugging, token text extraction, manual IDs and plugin order, duplicate handling, HTML-block limitations, browser use, source maps, and custom token renderers. Its permalink section is exceptional: it shows exact output and openly compares screen-reader link lists, translation behavior, Safari Reader, nested-link limits, assistive text, ARIA variants, and required CSS. A few type-definition details and peer-dependency friction still require package inspection.
Maintenance5/5Version 9.2.1 was published in July 2026 with a matching repository push and a targeted fix for inline-token level preservation in headerLink. Releases in 2024 added state-aware slugging and improved types rather than merely changing metadata. Seven open issues and pull requests include a new duplicate-ID proposal from August 2026, showing live discussion without a large abandoned backlog. The project also tests its built distributions and maintains ESM, CommonJS, UMD, source maps, and declarations.
Ecosystem5/5The package recorded 3,171,711 downloads in the measured week and the repository has 324 stars and 73 forks. It integrates directly with markdown-it-attrs for explicit IDs and recommends markdown-it-toc-done-right as a companion using the same anchor concerns. Custom slug libraries, callbacks, browser UMD, typed token access, and configurable renderers cover many static-site and documentation systems, while the peer dependency keeps markdown-it itself under the application's version control.

Use it if

  • Your renderer already uses markdown-it and needs heading IDs that match a table of contents or deep links
  • You want accessible permalink layouts rather than hand-inserting a visible hash into every heading
  • You need custom slug rules, per-document state, explicit author IDs, or a callback that collects heading metadata
  • You are willing to treat slug output as a permanent URL contract and test it across content changes
Skip it if

Setup reality

Install markdown-it-anchor alongside markdown-it. The npm metadata also declares @types/markdown-it as a peer, even for JavaScript consumers, so modern npm may install or warn about it depending on your dependency policy. The package has no runtime dependencies, native code, credentials, or configuration files and ships CommonJS, ESM, browser UMD, source maps, and its own declarations. The important setup decision is slug policy. Default slugs are lowercase, whitespace-collapsed, URI-encoded heading text; changing slugify later breaks incoming bookmarks and table-of-contents links. Supply a Unicode-aware slugger if readable non-ASCII and punctuation behavior matters, and lock examples in tests. Generated duplicates become slug-1, slug-2, starting at uniqueSlugStartIndex. Existing IDs from markdown-it-attrs are reused, but load that plugin before markdown-it-anchor; duplicate explicit IDs throw. Only text and inline-code tokens contribute to a slug by default, so images, raw inline HTML, and custom plugin tokens can make a visible heading differ from its fragment. Customize getTokensText if that mismatch matters. Heading elements receive tabindex=-1 unless tabIndex is false, a deliberate accessibility behavior that may affect DOM snapshots or styling. Permalinks are opt-in. Old boolean and legacy permalink options emit a deprecation warning; use anchor.permalink.headerLink, linkAfterHeader, linkInsideHeader, or ariaHidden. Each has real accessibility consequences. headerLink is simple but cannot contain an existing link and may need safariReaderFix. linkAfterHeader's recommended visually-hidden style requires both assistiveText and a CSS class that your stylesheet actually hides accessibly. The plugin mutates markdown-it's token stream, so plugin ordering and custom permalink code need integration tests.

Patterns

Add IDs to every Markdown headingadd-heading-ids

const MarkdownIt = require('markdown-it');
const anchor = require('markdown-it-anchor');

const md = new MarkdownIt().use(anchor);
const html = md.render('# Hello world');
// <h1 id="hello-world" tabindex="-1">Hello world</h1>

Permalinks are not inserted by default. The plugin adds an ID and tabindex=-1 to matching headings.

Anchor only selected heading levelsselect-heading-levels

const md = new MarkdownIt().use(anchor, {
  level: [2, 3],
});

A number means that level and deeper, while an array means exactly those levels. level: 2 includes h2 through h6.

Use a stable custom sluggercustomize-slugs

import slugify from '@sindresorhus/slugify';
import anchor from 'markdown-it-anchor';

md.use(anchor, {
  slugify: (title) => slugify(title),
});

The slugger is a URL compatibility decision. Pin its package version and test representative Unicode, punctuation, and duplicate headings before publishing links.

Prefix slugs with document stateslug-with-document-state

md.use(anchor, {
  slugifyWithState: (title, state) => {
    const local = title.trim().toLowerCase().replace(/\s+/g, '-');
    return `${state.env.documentId}-${local}`;
  },
});

md.render(source, { documentId: 'api-v2' });

slugifyWithState takes precedence over slugify. Ensure documentId is already URL-safe or encode it explicitly.

Let authors set a stable heading IDreuse-explicit-heading-id

const attrs = require('markdown-it-attrs');

const md = new MarkdownIt()
  .use(attrs)
  .use(anchor);

md.render('# Translated title {#account-settings}');

Load markdown-it-attrs first so anchor sees and reuses the ID. Duplicate explicit IDs throw instead of receiving automatic suffixes.

Wrap heading contents in a permalinkadd-header-link

const md = new MarkdownIt().use(anchor, {
  permalink: anchor.permalink.headerLink({
    safariReaderFix: true,
  }),
});

headerLink is accessible and simple, but a heading cannot legally contain another link. The Safari fix adds a span around heading contents.

Place an accessible permalink after each headingadd-accessible-permalink

const md = new MarkdownIt().use(anchor, {
  permalink: anchor.permalink.linkAfterHeader({
    style: 'visually-hidden',
    assistiveText: (title) => `Permalink to ${title}`,
    visuallyHiddenClass: 'sr-only',
    symbol: '#',
  }),
});

You must define an sr-only CSS class using an accessible visually-hidden technique. The renderer throws if required options are missing.

Collect titles and final slugs during renderingcollect-heading-index

const headings = [];
const md = new MarkdownIt().use(anchor, {
  callback: (_token, info) => headings.push(info),
});

const html = md.render(source);
// headings contains { title, slug } entries in render order

The callback sees the final unique slug after duplicate handling, making it safer than independently recreating IDs for a table of contents.

Include custom inline token text in slugscustomize-heading-text

md.use(anchor, {
  getTokensText(tokens) {
    return tokens
      .filter((token) => !['html_inline', 'image'].includes(token.type))
      .map((token) => token.content)
      .join('');
  },
});

The default includes only text and code_inline. Broadening token types can change old URLs when another markdown-it plugin changes its token output.

Leave tabindex off generated headingsdisable-heading-tabindex

md.use(anchor, { tabIndex: false });

The default -1 helps screen readers announce a heading after fragment navigation. Disable it only after checking the accessibility behavior you want.

Start duplicate slug suffixes at twocustomize-duplicate-suffix

md.use(anchor, { uniqueSlugStartIndex: 2 });

md.render('## Repeat\n\n## Repeat');
// IDs: repeat and repeat-2

This affects generated IDs only. Explicit duplicate IDs are treated as author errors and stop rendering.

Route permalinks through a document URLcustomize-permalink-url

md.use(anchor, {
  permalink: anchor.permalink.headerLink({
    renderHref: (slug, state) =>
      `/docs/${encodeURIComponent(state.env.docSlug)}#${slug}`,
    renderAttrs: () => ({ 'data-anchor': 'heading' }),
  }),
});

renderAttrs values become HTML attributes through markdown-it tokens. Keep user input out of attribute names and encode URL path components.

Alternatives

PackageRegistryPick it when
rehype-slugnpmYour Markdown pipeline uses unified and rehype rather than markdown-it
markdown-it-named-headingsnpmYou only need IDs on markdown-it headings and prefer a smaller, older feature set
markdown-it-headinganchornpmYou need classic named anchor elements in an existing markdown-it integration
markdown-it-toc-done-rightnpmYour real requirement is a matching table of contents; it is designed as this plugin's companion rather than a permalink replacement