Guides

Syntax Highlighting

Syntax Highlighting

TanStack Markdown does not bundle a tokenizer, language definitions, themes, CSS, or an asynchronous grammar runtime. It accepts one synchronous CodeHighlighter callback.

TanStack Highlight adapter

TanStack Markdown owns the surrounding <pre><code> elements. TanStack Highlight's high-level HTML methods also produce those containers, so they must not be passed directly as the callback. createTanStackMarkdownHighlighter() returns escaped markup for the inside of Markdown's <code> element:

The first-party adapter and renderer-owned theme selectors require @tanstack/highlight@0.0.6 or newer.

ts
// markdown-highlighter.ts
import { createHighlighter } from '@tanstack/highlight/core'
import { html } from '@tanstack/highlight/languages/html'
import { js } from '@tanstack/highlight/languages/js'
import { plaintext } from '@tanstack/highlight/languages/plaintext'
import { ts } from '@tanstack/highlight/languages/ts'
import { tsx } from '@tanstack/highlight/languages/tsx'
import { createTanStackMarkdownHighlighter } from '@tanstack/highlight/markdown'
import type { CodeHighlighter } from '@tanstack/markdown'

const highlighter = createHighlighter({
  languages: [plaintext, html, js, ts, tsx],
})

export const highlightMarkdownCode: CodeHighlighter =
  createTanStackMarkdownHighlighter(highlighter)

Register only the languages the application expects. Unknown identifiers degrade to escaped plain text.

Use the same callback with React:

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>
  )
}

Or with the HTML renderer:

ts
import { renderHtml } from '@tanstack/markdown/html'
import { highlightMarkdownCode } from './markdown-highlighter'

const html = renderHtml(source, {
  highlighter: highlightMarkdownCode,
  codeLineNumbers: true,
})

The callback receives the raw code, the fence language, parsed highlight lines, and the configured line-number preference. Highlighter output is inserted as trusted HTML, so use only an adapter that escapes source text.

Light and dark themes

createThemeCss defines Highlight's variables and token classes. Point its structural rules at Markdown's renderer-owned 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',
})}

.markdown-renderer .th-line--highlighted {
  background: color-mix(in srgb, var(--th-token) 10%, transparent);
}`

Wrap rendered content in an element with className="markdown-renderer". When codeLineNumbers is true, every renderer adds tm-code--line-numbers to the Markdown-owned <pre>.

Fence metadata

markdown
```tsx file="app.tsx" framework="react" {2,4-6}
const one = 1
const two = 2
```

The parser records metadata even when no highlighter is configured. This keeps content parsing independent from presentation and lets a build pipeline change highlighters without rebuilding the AST.

Keep it server-only when possible

For static blogs and docs, parse and highlight during a build or server render, then send the finished markup. The Markdown package does not force highlighting or registered language grammars into client bundles.