Model replies include fenced code. TextPart renders those fences as plain text until you pass a highlighter.
Build a TanStack Highlight callback. Pass it to TextPart. Token colors then show on fenced blocks.
If your app still passes remarkPlugins or rehypePlugins, start with TextPart markdown.
pnpm add @tanstack/highlightUse @tanstack/highlight 0.0.6 or newer. The Markdown adapter lives in that package.
Create src/markdown-highlighter.ts. Register only the languages your models emit.
import { createHighlighter } from '@tanstack/highlight/core'
import { js } from '@tanstack/highlight/languages/js'
import { json } from '@tanstack/highlight/languages/json'
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, js, json, ts, tsx],
})
export const highlightMarkdownCode: CodeHighlighter =
createTanStackMarkdownHighlighter(highlighter)Unknown fence languages render as escaped plain text.
CAUTION: Highlighter output is inserted as trusted HTML. Use createTanStackMarkdownHighlighter. Do not pass a highlighter that returns unescaped source text.
Create src/markdown-highlight-theme.ts. Point the selectors at a .markdown-renderer wrapper.
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',
})Put markdown-renderer on the TextPart wrapper so the theme CSS matches.
import { TextPart } from '@tanstack/ai-react/ui'
import { highlightMarkdownCode } from './markdown-highlighter'
export function Reply({ content }: { content: string }) {
return (
<TextPart
content={content}
highlighter={highlightMarkdownCode}
className="markdown-renderer"
/>
)
}Inject the CSS once in layout. Register TextPart as partsComponents.text.
import { fetchServerSentEvents } from '@tanstack/ai-react'
import { createChatHook, TextPart } from '@tanstack/ai-react/ui'
import { highlightMarkdownCode } from './markdown-highlighter'
import { markdownHighlightCss } from './markdown-highlight-theme'
const chatOptions = {
connection: fetchServerSentEvents('/api/chat'),
}
const { useAppChat, useChatContext } = createChatHook({
options: chatOptions,
components: {
input: () => {
const chat = useChatContext()
return (
<form
onSubmit={(event) => {
event.preventDefault()
const field = event.currentTarget.elements.namedItem('message')
if (!(field instanceof HTMLInputElement)) return
const text = field.value.trim()
if (!text) return
field.value = ''
void chat.sendMessage(text)
}}
>
<input name="message" />
<button type="submit">Send</button>
</form>
)
},
layout: ({ Messages, Input }) => (
<main>
<style>{markdownHighlightCss}</style>
<Messages />
<Input />
</main>
),
message: ({ message, Parts }) => (
<article data-role={message.role}>
<Parts />
</article>
),
},
partsComponents: {
text: ({ part }) => (
<TextPart
content={part.content}
highlighter={highlightMarkdownCode}
className="markdown-renderer"
/>
),
fallback: () => null,
},
})
export function ChatScreen() {
const chat = useAppChat()
return <chat.AppChat />
}Send a prompt that returns a fenced ts block. The fence now shows token colors.
Use the same highlighter file and the same theme file. The TextPart import changes. Inject markdownHighlightCss once at the app root.
import { defineComponent, h } from 'vue'
import { TextPart } from '@tanstack/ai-vue/ui'
import { highlightMarkdownCode } from './markdown-highlighter'
export default defineComponent({
props: { content: { type: String, required: true } },
setup(props) {
return () =>
h(TextPart, {
content: props.content,
highlighter: highlightMarkdownCode,
class: 'markdown-renderer',
})
},
})import { TextPart } from '@tanstack/ai-solid/ui'
import { highlightMarkdownCode } from './markdown-highlighter'
export function Reply(props: { content: string }) {
return (
<TextPart
content={props.content}
highlighter={highlightMarkdownCode}
class="markdown-renderer"
/>
)
}Vue and Solid have no components map. Tag swaps go through a TanStack Markdown extension.