Mantle CodeBlock Migration Guide
This document describes how to migrate from PrismJS-powered code blocks to @ngrok/mantle's Shiki-powered CodeBlock component. There are three packages involved:
Key design principle: syntax highlighting never runs in the browser. It happens at build time (Vite plugin), at request time (server highlighter), or not at all (graceful fallback to plain text).
Security note:
preHtmlis rendered viadangerouslySetInnerHTML. Only pass HTML produced by Shiki (via the Vite plugin, server highlighter, or highlight server). Never pass unsanitized user input aspreHtml.
Codemod
Codemod: manual: the move from PrismJS to CodeBlock swaps one library's API for another, so no rewrite rule can name the target shape.
Migrating from PrismJS (before → after)
If your app currently uses PrismJS (directly or via react-syntax-highlighter), here are the patterns you need to find and replace.
What to search for (old patterns)
Look for these imports and usages in your codebase:
Replacement patterns (new)
Key differences from PrismJS
CSS migration
Remove PrismJS theme CSS imports and any custom Prism token styles:
Mantle's mantle.css already includes the Shiki CSS variable theme. Import it:
Uninstall old packages
Use case 1: React 19 + Vite + Node server (e.g. React Router 7 / Express)
This is the most common setup. You have a Vite-built React app served by a Node.js server (React Router 7, Express, Fastify, etc.). You want:
- Build-time highlighting for static code in components (via
mantleCodetagged templates) - Server-side highlighting for dynamic code (via an API route using
@ngrok/mantle-server-syntax-highlighter) - MDX fenced code block highlighting (if you use MDX)
1. Install dependencies
2. Configure Vite
Plugin options
mantleCodeBlockPlugins() accepts an options object:
If you don't use MDX, disable it: mantleCodeBlockPlugins({ mdx: false }).
If you only use MDX and not tagged templates: mantleCodeBlockPlugins({ runtime: false }).
3. Use CodeBlock in components (build-time highlighting)
The Vite plugin rewrites mantleCode("typescript")\...`at build time, injecting pre-rendered Shiki HTML into the~preHtml` property. No Shiki code ships to the browser.
mantleCode options
Indentation defaults
Languages have default indentation styles. Tab-indented by default: csharp, css, go, html, java, javascript, js, jsx, typescript, ts, tsx, xml. All other languages default to spaces unless overridden. Use the indentation option to override.
Interpolation
Interpolated values are replaced at runtime via placeholder substitution. The Vite plugin highlights the template with placeholders, and the component swaps in real values at render time. The copy button copies the plain-text version with real values.
Limitation: If an interpolated value changes the token type at that position (e.g., injecting a bare identifier where the placeholder sits inside string quotes), the syntax highlighting color of the surrounding span may be incorrect. This is acceptable for typical usage where interpolated values appear inside strings, template expressions, or command substitutions.
4. Server-side highlighting for dynamic code
For code that isn't known at build time (user input, API responses, database content), use the server syntax highlighter in an API route or server action.
Then on the client, fetch from the API and construct a MantleCodeBlockValue:
5. MDX fenced code blocks (if using MDX)
If you use MDX, fenced code blocks are automatically highlighted at build time via the rehype plugin. You need to map the <pre> element to a CodeBlock component in your MDX provider:
MDX fenced code blocks support metadata in the opening fence:
```tsx title="example.tsx" mode=file showLineNumbers highlight="2-3"
const x = 1;
const y = 2;
const z = x + y;
```
Supported meta keys: title, mode (cli | file | traffic-policy), showLineNumbers, highlight / highlightLines, lineNumberStart, collapsible, disableCopy.
Full example with all meta keys:
```tsx title="example.tsx" mode=file showLineNumbers highlight="2-4" lineNumberStart=10 collapsible disableCopy
const x = 1;
const y = 2;
const z = x + y;
```
Use case 2: React 19 + Vite + MDX static content
This is for static sites or documentation sites where all code is known at build time in MDX files. No server-side highlighting is needed.
1. Install dependencies
You do NOT need @ngrok/mantle-server-syntax-highlighter because all code is static and highlighted at build time.
2. Configure Vite
3. Write MDX with fenced code blocks
# Getting Started
Install the package:
```bash mode=cli title="Command Line"
npm install @ngrok/mantle
```
Then use it:
```tsx title="app.tsx" highlight="3"
import { Button } from "@ngrok/mantle/button";
function App() {
return (
<Button appearance="filled" intent="neutral">
Click me
</Button>
);
}
```
All fenced code blocks are pre-rendered to HTML at build time. No Shiki ships to the browser.
4. Set up the MDX provider
Same as Use Case 1, Step 5. Map <pre> to CodeBlock in your MDX provider using resolvePreRenderedCodeBlockProps.
5. Use mantleCode in React components (optional)
If you also have code blocks in React components (not MDX), use mantleCode tagged templates exactly as shown in Use Case 1, Step 3. The Vite plugin handles both MDX and tagged templates.
What you DON'T need
- No
@ngrok/mantle-server-syntax-highlighter: all highlighting is build-time - No API routes for highlighting
- No
createMantleCodeBlockValueunless you're composing values manually in React components
Use case 3: Vite SPA + Go server
This is for apps where the frontend is a Vite-built React SPA and the backend is a Go server. The Go server cannot run Shiki (it's JavaScript/Wasm), so you have two options for dynamic code:
Option A: Use mantleCode tagged templates for all static code (build-time, via Vite plugin) and skip server highlighting entirely. Code not known at build time renders as plain text (no syntax highlighting).
Option B: Run a sidecar Node.js highlighting service that the frontend calls directly for dynamic code, using @ngrok/mantle-server-syntax-highlighter to produce highlighted HTML.
1. Install dependencies
2. Configure Vite
3. Use CodeBlock in components (build-time highlighting)
Exactly the same as Use Case 1, Step 3. mantleCode tagged templates are transformed at Vite build time; the Go server is not involved.
4. Dynamic code highlighting (option A: plain text fallback)
When ~preHtml is omitted, CodeBlock falls back to rendering the code as escaped plain text (no colors, but still functional with copy button, line numbers, etc.).
4. Dynamic code highlighting (option B: sidecar highlight service)
Run a small Node.js sidecar alongside your Go server that uses @ngrok/mantle-server-syntax-highlighter to highlight code on demand. A minimal service should expose an HTTP endpoint like the following:
Response:
Then fetch from the frontend and construct a MantleCodeBlockValue:
React 19 requirement
As of this release, @ngrok/mantle requires react@^19 and react-dom@^19.
Component reference
Compound component structure
Tabbed code blocks
Use tabs when showing the same concept in multiple languages:
For controlled tabs, use activeTab and onActiveTabChange instead of defaultTab.
Collapsible code blocks
Use ExpanderButton for long code blocks. The collapsed height is approximately 4 lines (~13.6rem):
In MDX, use the collapsible meta key:
```typescript collapsible
// Long code block that will be collapsed by default
```
Supported languages (28)
bash, cs, csharp, css, go, html, java, javascript, js, json, jsx, plain, plaintext, py, python, rb, ruby, rust, sh, shell, text, ts, tsx, txt, typescript, xml, yaml, yml
Many are aliases: js↔javascript, ts↔typescript, py↔python, rb↔ruby, sh↔bash↔shell, cs↔csharp, yml↔yaml, plain↔plaintext↔text↔txt. Unsupported languages fall back to "text" (plain text, no highlighting).
Key imports
Highlight server API
The sidecar highlight server accepts POST / with:
Responds with { code, html, language, highlightLines, lineNumberStart, showLineNumbers }.
Health check: GET /health returns 200 {"status":"ok"} when ready, 503 {"status":"starting"} during Shiki preload.
CSS / theming
CodeBlock uses Mantle's built-in CSS variable theme (mantle-css-variables). Import the Mantle CSS in your app:
The Mantle ThemeProvider handles light/dark mode automatically. The highlighted HTML uses CSS variable class names that respond to the active theme.
Troubleshooting & gotchas
mantleCode returns plain text (no syntax highlighting)
The Vite plugin is not configured. mantleCode is a no-op at runtime: it requires the Vite plugin to transform tagged templates at build time. Add mantleCodeBlockPlugins().vitePlugins to your Vite config. In development, you may see a warning in the console.
Unsupported language silently falls back to plain text
If you pass a language string that isn't in the supported list, parseLanguage returns "text" (no highlighting, no error). Check isSupportedLanguage(lang) if you need to detect this.
Highlight line 0 is silently ignored
Line numbers are 1-based. Passing 0 in highlightLines (e.g., [0, 3]) silently filters it out. Ranges like "0-3" are also filtered.
preHtml must come from a trusted source
preHtml is rendered via dangerouslySetInnerHTML. Only pass HTML produced by Shiki through the Vite plugin, createMantleServerSyntaxHighlighter, or the highlight server. Never pass HTML strings that came from a user.
Tab width is hardcoded to 2 spaces
The <pre> element renders with tab-size: 2. This is not configurable via props or options.
Title whitespace is trimmed
The title meta key value is trimmed: leading/trailing whitespace is removed.
CodeBlock works without CopyButton, Header, or ExpanderButton
All sub-components besides Root, Body, and Code are optional. A minimal code block is: