Guides

Markdown Pipelines

Markdown Pipelines

TanStack Highlight provides helpers at three different Markdown boundaries. All accept an explicit highlighter so adapters do not import every language.

TanStack Markdown

TanStack Markdown owns the surrounding <pre><code> elements. Use the dedicated adapter to return only Highlight's escaped inner token markup:

ts
import { createTanStackMarkdownHighlighter } from '@tanstack/highlight/markdown'
import { highlighter } from './highlight'

export const highlightMarkdownCode =
  createTanStackMarkdownHighlighter(highlighter)

Pass the same callback to the React or HTML renderer:

tsx
import { Markdown } from '@tanstack/markdown/react'
import { highlightMarkdownCode } from './markdown-highlighter'

export function Article({ source }: { source: string }) {
  return (
    <Markdown highlighter={highlightMarkdownCode} codeLineNumbers>
      {source}
    </Markdown>
  )
}

The adapter maps parsed highlighted lines to th-line--highlighted, preserves line-number wrappers, escapes source text, and degrades unknown languages to escaped plaintext. It does not import TanStack Markdown or any languages.

Generate theme CSS against Markdown's wrapper classes:

ts
import { createThemeCss } from '@tanstack/highlight/theme'
import { githubDarkTheme } from '@tanstack/highlight/themes/github-dark'
import { githubLightTheme } from '@tanstack/highlight/themes/github-light'

export const markdownHighlightCss = createThemeCss({
  light: githubLightTheme,
  dark: githubDarkTheme,
  lightSelector: '.markdown-renderer',
  darkSelector: '.dark .markdown-renderer',
  codeBlockSelector: '.markdown-renderer pre.tm-code',
  lineNumbersSelector: '.markdown-renderer .tm-code--line-numbers',
})

Wrap the rendered document in .markdown-renderer. Add application CSS for .th-line--highlighted if highlighted lines need a background.

Direct fence rendering

ts
import { renderCodeFence } from '@tanstack/highlight/markdown'
import { highlighter } from './highlight'

const rendered = renderCodeFence(
  {
    code: `const answer = 42\n`,
    lang: 'ts',
    meta: 'title="answer.ts" {1} lineNumbers',
  },
  highlighter,
)

The result includes:

ts
rendered.copyText
rendered.htmlMarkup
rendered.lang
rendered.title
rendered.tokens
rendered.decorations
rendered.lineNumbers

copyText trims trailing whitespace from the block before highlighting.

Remark

Use the Remark plugin before remark-rehype:

ts
import { remarkHighlightCodeBlocks } from '@tanstack/highlight/remark'

const highlightCode = remarkHighlightCodeBlocks({
  highlighter,
  lineNumbers: false,
  getTitle(node) {
    return node.data?.filename as string | undefined
  },
  getDecorations(node) {
    return node.lang === 'diff'
      ? [{ lines: 1, className: 'diff-context' }]
      : undefined
  },
})

The plugin replaces mdast code nodes with a highlightedCode extension node carrying hName, hProperties, and hChildren. remark-rehype therefore receives structured HAST without enabling raw HTML.

For pipelines that intentionally consume raw HTML, remarkCodeNodeToHtml() returns a raw mdast HTML node. Prefer the structured path by default.

Rehype

Use the Rehype plugin when the tree already contains <pre><code class="language-*">:

ts
import { rehypeHighlightCodeBlocks } from '@tanstack/highlight/rehype'

const highlightCode = rehypeHighlightCodeBlocks({
  highlighter,
  lineNumbers: true,
})

The plugin:

  1. Finds <pre> elements with a <code> child.
  2. Reads the first language-* class from the code element.
  3. Collects text from its descendants.
  4. Replaces the node with highlighted HAST.

Already highlighted th-code blocks are skipped, making the transform idempotent.

HAST helpers

ts
import {
  codeFenceToHast,
  parseCodeFenceMeta,
  tokensToHast,
} from '@tanstack/highlight/markdown'

Use these when a custom parser already owns traversal but wants the same output contract.

Markdown language versus Markdown adapter

They solve different problems:

  • languages/markdown highlights Markdown source as a code sample and can delegate its internal fences.
  • remark and rehype transform the code fences of a rendered Markdown document.

Most documentation sites need the adapter. Register the Markdown language only when they display Markdown source inside a code block.

@octanejs/mdx users can wrap the Rehype adapter in its expected plugin tuple with createOctaneMdxHighlight().