mrkeyoor.com_
Wed 23 Sept 02:54 UTC
npmUtilsupdated 22 Sept 2026

markdown-it-anchor review

markdown-it-anchor 9.2.1 is a markdown-it plugin that gives rendered headings unique id attributes and can add permalink markup. Its default slugger lowercases heading text, collapses whitespace to hyphens, and URI-encodes the result; duplicate generated slugs receive numeric suffixes. Existing IDs from an earlier plugin are preserved, while duplicate explicit IDs stop rendering with an error. Version 9.2.1 fixes token-level preservation in the headerLink permalink style. The package includes CommonJS and ESM builds, declarations, several accessibility-aware permalink renderers, callbacks for collecting final heading data, and custom slug functions that can inspect render state.

Verdict

markdown-it-anchor 9.2.1 added 6.4 KB minified and 2.1 KB gzipped in our browser build, with 0 direct dependencies and 0 audit findings. It is the practical markdown-it choice for heading IDs, provided the team locks slug output and selects a permalink renderer after testing links, focus, translation, and reader mode.

We installed it

Lab card: what happened when we installed markdown-it-anchorScreenshot of markdown-it-anchor documentation
Install✓ · 2s12 packages on disk · 4 MB
ImportESM import works · require() works · CommonJS package
Browser2.1 KBgzipped (6.4 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does markdown-it-anchor install cleanly?

Yes. In a fresh container with an empty cache, npm install markdown-it-anchor finished in 2 seconds, leaving 12 packages and 4 MB on disk. npm audit reported no known vulnerabilities.

How much does markdown-it-anchor add to a browser bundle?

2.1 KB gzipped (6.4 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does markdown-it-anchor work with both ESM and CommonJS?

Yes. Both import 'markdown-it-anchor' and require('markdown-it-anchor') worked in Node 22 in our run. The package is published as CommonJS.

Does markdown-it-anchor include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

markdown-it-anchor or markdown-it-headinganchor: which should you use?

markdown-it-headinganchor: Use it for an older markdown-it codebase that specifically expects separate named anchor elements. markdown-it-anchor 9.2.1 added 6.4 KB minified and 2.1 KB gzipped in our browser build, with 0 direct dependencies and 0 audit findings.

When should you not use markdown-it-anchor?

Your pipeline does not use markdown-it. The package declares markdown-it and @types/markdown-it as peers and operates on markdown-it tokens.

API stability4/5Registration through md.use(anchor, options), level selection, slug callbacks, existing-ID reuse, and the final callback remain a compact contract in version 9.2.1. The version 8 permalink redesign moved users toward named renderers and left old options behind deprecation warnings. Public HTML and fragments still make configuration changes risky: changing a slugger, duplicate index, tabIndex, or renderer can break inbound URLs, CSS, DOM snapshots, and assistive behavior without changing JavaScript call sites.
Docs5/5The README documents default slug construction, duplicate suffixes, explicit-ID ordering, raw HTML limits, state-aware slugging, callbacks, token text selection, browser builds, source maps, and every top-level option. Its permalink section prints the resulting HTML and explains concrete drawbacks involving nested links, Safari Reader, screen-reader link lists, translated aria-label text, and required visually-hidden CSS. Package inspection is still needed to notice that @types/markdown-it is a declared peer.
Maintenance5/5Version 9.2.1 and the repository push both landed on July 6, 2026. That patch preserves inline token levels in headerLink output, a narrow rendering correction rather than a metadata-only release. The repository is unarchived, has 324 stars, and current work follows earlier version 9 additions for state-aware slugging and stronger declarations. The release list is sparse because many versions are tags rather than richly described GitHub releases, but the npm line is current.
Ecosystem5/5npm counted 3,155,222 downloads in the latest completed week. The package works with markdown-it-attrs for hand-authored IDs and markdown-it-toc-done-right for matching navigation, while custom sluggers can reproduce another site's URL rules. CommonJS, ESM, a UMD browser build, source maps, and bundled declarations cover several build systems. The 2 peer dependencies keep the renderer and its types outside this package's runtime graph.

Use it if

  • A markdown-it renderer needs stable fragment URLs that can be shared, indexed, and matched by a table of contents.
  • Heading authors need explicit IDs through markdown-it-attrs, with generated IDs used for everything else.
  • Permalink markup must account for screen-reader link lists, translated assistive text, and Safari Reader behavior.
  • A build step needs each final title and de-duplicated slug through a callback instead of reimplementing the slug algorithm.
Skip it if

Setup reality

We installed markdown-it-anchor 9.2.1 in a fresh Node 22 Bookworm sandbox. npm completed in 2 seconds, left 12 packages using 4 MB, and reported 0 known vulnerabilities. The plugin itself is 168 KB unpacked with 0 direct dependencies and 2 peers. require() and ESM import both worked from its CommonJS entry without an exports map, and TypeScript declarations are included. Our browser bundle measured 6.4 KB minified and 2.1 KB gzipped.

Install markdown-it as the runtime peer. npm metadata also lists @types/markdown-it as a peer, which can matter under strict peer policies even though plain JavaScript does not consume those types. There are no credentials, native builds, or config files. Slug policy is the real setup work: once links are public, changing slugify, Unicode treatment, or uniqueSlugStartIndex changes URLs. Keep representative headings in snapshot tests.

Plugin order controls explicit IDs. markdown-it-attrs must run before markdown-it-anchor so its ID is visible; repeated author IDs then throw instead of being renamed. Only text and inline-code tokens contribute to the default title used for slugging. Images, raw inline HTML, and custom inline tokens can make the visible heading differ from its fragment unless getTokensText is replaced.

Headings receive tabindex=-1 by default to support focus after fragment navigation. Permalinks are opt-in, and each renderer has a different DOM cost. headerLink cannot wrap a heading that already contains a link and may need safariReaderFix. linkAfterHeader needs real assistive text and, for its visually-hidden style, CSS that hides content without removing it from accessibility APIs.

Patterns

Generate IDs without permalinks add-heading-ids

const MarkdownIt = require('markdown-it')
const anchor = require('markdown-it-anchor')
const md = new MarkdownIt().use(anchor)
console.log(md.render('# Account settings'))

The default output has id=account-settings and tabindex=-1. No permalink link is added unless configured.

Limit anchors to h2 and h3 select-levels

md.use(anchor, { level: [2, 3] })

An array selects exact levels. Passing level: 2 means h2 through h6 instead.

Replace the default slug policy use-github-slugs

import GithubSlugger from 'github-slugger'
const slugger = new GithubSlugger()
md.use(anchor, { slugify: (title) => slugger.slug(title) })

Reset or recreate GithubSlugger for each document, or duplicate counters will leak across renders.

Include document state in fragments slug-with-state

md.use(anchor, {
  slugifyWithState(title, state) {
    return `${state.env.section}-${encodeURIComponent(title.toLowerCase())}`
  },
})
md.render(source, { section: 'api-v2' })

slugifyWithState overrides slugify. Encode every state value that can contain URL punctuation.

Honor an author's stable ID reuse-explicit-id

const attrs = require('markdown-it-attrs')
const md = new MarkdownIt().use(attrs).use(anchor)
md.render('# Translated heading {#billing}')

Load markdown-it-attrs first. Two explicit #billing IDs cause an error rather than automatic renaming.

Make the heading itself the permalink wrap-heading-link

md.use(anchor, {
  permalink: anchor.permalink.headerLink({ safariReaderFix: true }),
})

headerLink cannot contain another link. safariReaderFix inserts a span so Safari Reader keeps the heading.

Place translated link text after a heading add-assisted-link

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

Define sr-only with an accessibility-safe hiding recipe; display:none would remove the text from assistive technology.

Build an index from final slugs collect-headings

const headings = []
md.use(anchor, { callback: (_token, info) => headings.push(info) })
const html = md.render(source)

info contains the title and the final unique slug after collision handling, which keeps a table of contents in sync.

Choose which inline tokens form a slug change-token-text

md.use(anchor, {
  getTokensText(tokens) {
    return tokens.filter((t) => t.type === 'text' || t.type === 'code_inline')
      .map((t) => t.content).join('')
  },
})

The shown filter matches the default idea. Add custom token types only after checking how they change already published URLs.

Keep headings out of programmatic focus disable-tabindex

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

The default is tabindex=-1 for fragment focus. Test keyboard and screen-reader navigation before removing it.

Begin duplicate suffixes at two set-duplicate-index

md.use(anchor, { uniqueSlugStartIndex: 2 })
md.render('## Repeat\n\n## Repeat')

Generated IDs become repeat and repeat-2. This option does not repair duplicate IDs supplied by authors.

Point fragments at a canonical document URL customize-link-target

md.use(anchor, {
  permalink: anchor.permalink.headerLink({
    renderHref: (slug, state) => `/docs/${encodeURIComponent(state.env.doc)}#${slug}`,
  }),
})

renderHref controls the emitted href. Keep the slug unchanged if existing external fragment links must continue working.

Alternatives

PackageRegistryPick it when
markdown-it-headinganchornpmUse it for an older markdown-it codebase that specifically expects separate named anchor elements.
markdown-it-toc-done-rightnpmUse it alongside an anchor policy when the missing feature is a generated table of contents.
github-sluggernpmUse it when GitHub-compatible unique slug generation is needed outside markdown-it or as a custom slug source.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.