TanStack Highlight provides helpers at three different Markdown boundaries. All accept an explicit highlighter so adapters do not import every language.
TanStack Markdown owns the surrounding <pre><code> elements. Use the dedicated adapter to return only Highlight's escaped inner token markup:
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:
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:
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.
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:
rendered.copyText
rendered.htmlMarkup
rendered.lang
rendered.title
rendered.tokens
rendered.decorations
rendered.lineNumberscopyText trims trailing whitespace from the block before highlighting.
Use the Remark plugin before remark-rehype:
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.
Use the Rehype plugin when the tree already contains <pre><code class="language-*">:
import { rehypeHighlightCodeBlocks } from '@tanstack/highlight/rehype'
const highlightCode = rehypeHighlightCodeBlocks({
highlighter,
lineNumbers: true,
})The plugin:
Already highlighted th-code blocks are skipped, making the transform idempotent.
import {
codeFenceToHast,
parseCodeFenceMeta,
tokensToHast,
} from '@tanstack/highlight/markdown'Use these when a custom parser already owns traversal but wants the same output contract.
They solve different problems:
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().