Skip to main content

Best Markdown Parsing Libraries for JavaScript in 2026

·PkgPulse Team

TL;DR

marked for speed and simplicity; remark/unified for plugin-rich content pipelines. marked (~15M weekly downloads) is the fastest Markdown-to-HTML converter — parse a large doc in under 1ms, great for simple rendering. remark (~8M downloads) is the center of the unified ecosystem with 300+ plugins for linting, transforming, and generating documentation. markdown-it (~20M downloads) is the most popular by downloads with excellent plugin support and spec compliance.

Key Takeaways

  • markdown-it: ~20M weekly downloads — spec-compliant, widely used, great plugin support
  • marked: ~15M downloads — fastest, simplest API, GitHub-flavored Markdown
  • remark: ~8M downloads — unified ecosystem, 300+ plugins, AST-based transformations
  • marked + DOMPurify — always sanitize HTML output in browser contexts
  • remark — powers MDX, Docusaurus, Gatsby, most static site generators

// markdown-it — configurable, spec-compliant
import markdownIt from 'markdown-it';

const md = new markdownIt({
  html: false,          // Allow HTML tags in source
  linkify: true,        // Autoconvert URL-like text to links
  typographer: true,    // Enable some language-neutral typographic replacements
  breaks: false,        // Convert '\n' in paragraphs to <br>
  highlight: (str, lang) => {
    // Syntax highlighting with highlight.js or shiki
    if (lang && hljs.getLanguage(lang)) {
      return hljs.highlight(str, { language: lang }).value;
    }
    return '';
  },
});

const html = md.render('# Hello World\n\nThis is **markdown**.');
// <h1>Hello World</h1>
// <p>This is <strong>markdown</strong>.</p>
// markdown-it — plugins
import markdownIt from 'markdown-it';
import markdownItAnchor from 'markdown-it-anchor';
import markdownItToc from 'markdown-it-table-of-contents';
import markdownItFootnote from 'markdown-it-footnote';
import markdownItContainer from 'markdown-it-container';

const md = new markdownIt()
  .use(markdownItAnchor, {
    permalink: markdownItAnchor.permalink.ariaHidden({ placement: 'before' }),
  })
  .use(markdownItToc, { includeLevel: [2, 3] })
  .use(markdownItFootnote)
  .use(markdownItContainer, 'warning', {
    render: (tokens, idx) => {
      return tokens[idx].nesting === 1
        ? '<div class="warning">\n'
        : '</div>\n';
    },
  });

const html = md.render(`
[[toc]]

# Title

## Section 1

Text with footnote.[^1]

::: warning
This is a warning container.
:::

[^1]: The footnote text.
`);

marked (Fastest)

// marked — simple, fast Markdown to HTML
import { marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
import hljs from 'highlight.js';

// Configure once, use everywhere
marked.use(
  markedHighlight({
    langPrefix: 'hljs language-',
    highlight(code, lang) {
      const language = hljs.getLanguage(lang) ? lang : 'plaintext';
      return hljs.highlight(code, { language }).value;
    },
  })
);

// Parse Markdown to HTML
const html = marked('# Hello\n\nWorld!');
// <h1>Hello</h1><p>World!</p>

// Async version (for async rendering extensions)
const htmlAsync = await marked.parse('# Hello');
// marked — custom renderer
import { marked, Renderer } from 'marked';

const renderer = new Renderer();

// Custom link renderer — add target="_blank" to external links
renderer.link = (href, title, text) => {
  const isExternal = href?.startsWith('http');
  const titleAttr = title ? ` title="${title}"` : '';
  const target = isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
  return `<a href="${href}"${titleAttr}${target}>${text}</a>`;
};

// Custom heading — add IDs
renderer.heading = (text, level) => {
  const id = text.toLowerCase().replace(/[^\w]+/g, '-');
  return `<h${level} id="${id}">${text}</h${level}>\n`;
};

marked.use({ renderer });

const html = marked('# My Title\n\n[Google](https://google.com)');
// marked — IMPORTANT: sanitize in browser!
import { marked } from 'marked';
import DOMPurify from 'dompurify';

// NEVER inject untrusted markdown directly into innerHTML
// Always sanitize first
function renderMarkdown(userContent: string): string {
  const rawHtml = marked(userContent);
  return DOMPurify.sanitize(rawHtml); // XSS protection
}

// React usage
function MarkdownRenderer({ content }: { content: string }) {
  return (
    <div
      dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }}
    />
  );
}

remark + unified (Ecosystem)

// remark — AST-based, plugin-rich pipeline
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeSanitize from 'rehype-sanitize';
import rehypeStringify from 'rehype-stringify';
import rehypeHighlight from 'rehype-highlight';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';

// Full pipeline: Markdown → mdast → hast → HTML
const processor = unified()
  .use(remarkParse)           // Parse Markdown to mdast
  .use(remarkGfm)             // GitHub Flavored Markdown (tables, strikethrough)
  .use(remarkRehype)          // Convert mdast to hast (HTML AST)
  .use(rehypeSanitize)        // Sanitize HTML (safe by default)
  .use(rehypeHighlight)       // Syntax highlighting
  .use(rehypeSlug)            // Add IDs to headings
  .use(rehypeAutolinkHeadings) // Link headings to themselves
  .use(rehypeStringify);      // Stringify hast to HTML

const result = await processor.process('# Hello World');
const html = String(result);
// remark — lint Markdown files
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkLint from 'remark-lint';
import remarkLintNoDeadUrls from 'remark-lint-no-dead-urls';
import remarkLintNoMissingBlankLines from 'remark-lint-no-missing-blank-lines';

const file = await unified()
  .use(remarkParse)
  .use(remarkLint)
  .use(remarkLintNoDeadUrls)
  .use(remarkLintNoMissingBlankLines)
  .process(markdownContent);

// file.messages contains lint warnings/errors
console.log(file.messages);
// remark — custom AST transformation
import { visit } from 'unist-util-visit';
import type { Root } from 'mdast';

// Custom plugin: add "pkgpulse" data attribute to package name links
function remarkPkgPulseLinks() {
  return (tree: Root) => {
    visit(tree, 'link', (node) => {
      if (node.url.includes('pkgpulse.com/compare')) {
        node.data = node.data || {};
        node.data.hProperties = { 'data-pkgpulse': 'true' };
      }
    });
  };
}

MDX (Markdown + React)

// MDX — Markdown with embedded React components
// Powers: Next.js blog, Docusaurus, Gatsby, Storybook
import { MDXRemote } from 'next-mdx-remote';
import { serialize } from 'next-mdx-remote/serialize';
import remarkGfm from 'remark-gfm';

// In getStaticProps
const mdxSource = await serialize(markdownContent, {
  mdxOptions: {
    remarkPlugins: [remarkGfm],
  },
});

// In component
const components = {
  CodeBlock: ({ code, language }) => (
    <SyntaxHighlighter language={language}>{code}</SyntaxHighlighter>
  ),
  Callout: ({ children, type }) => (
    <div className={`callout callout-${type}`}>{children}</div>
  ),
};

<MDXRemote {...mdxSource} components={components} />

Performance Comparison

LibraryParse 10K wordsBundle SizeSecurity
marked~3ms~43KBManual sanitize needed
markdown-it~8ms~52KBSanitized by default
remark~15ms~200KB+ pluginsrehype-sanitize

When to Choose

ScenarioPick
Simple Markdown rendering in browsermarked + DOMPurify
Comment system, user-generated contentmarkdown-it (sanitized default)
Documentation site (Docusaurus, etc.)remark/unified
MDX (Markdown + React components)remark (MDX uses it)
Custom AST transforms neededremark (AST-first design)
Blog with code highlighting + TOCmarkdown-it or remark
Speed is the only prioritymarked

Compare Markdown library package health on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.