Skip to main content

Code Block

Code blocks render and apply syntax highlighting to blocks of code. The mantleCodeBlockPlugins() Vite plugin highlights the code at build time with Shiki, so there is zero highlighting cost in the browser.

Use mantleCode("language") tagged template literals to define code values. The Vite plugin transforms these at build time, inlining pre-rendered HTML.

ngrok-example.js

const listener = await ngrok.connect({	// session configuration	addr: `localhost:8080`, // or `8080` or `unix:${UNIX_SOCKET}`	authtoken: "<authtoken>",	authtoken_from_env: true,	on_status_change: (addr, error) => {		console.log(`disconnected, addr ${addr} error: ${error}`);	},	session_metadata: "Online in One Line",	// listener configuration	allow_user_agent: "^mozilla.*",	basic_auth: ["ngrok:online1line"],	circuit_breaker: 0.1,	compression: true,	deny_user_agent: "^curl.*",	domain: "<domain>",	ip_restriction_allow_cidrs: ["0.0.0.0/0"],	ip_restriction_deny_cidrs: ["10.1.1.1/32"],	metadata: "example listener metadata from nodejs",	mutual_tls_cas: [fs.readFileSync('ca.crt', 'utf8')],	oauth_provider: "google",	oauth_allow_domains: ["<domain>"],	oauth_allow_emails: ["<email>"],	oauth_scopes: ["<scope>"],	oauth_client_id: "<id>",	oauth_client_secret: "<secret>",	oidc_issuer_url: "<url>",	oidc_client_id: "<id>",	oidc_client_secret: "<secret>",	oidc_allow_domains: ["<domain>"],	oidc_allow_emails: ["<email>"],	oidc_scopes: ["<scope>"],	proxy_proto: "", // One of: "", "1", "2"	request_header_remove: ["X-Req-Nope"],	response_header_remove: ["X-Res-Nope"],	request_header_add: ["X-Req-Yup:true"],	response_header_add: ["X-Res-Yup:true"],	schemes: ["HTTPS"],	verify_webhook_provider: "twilio",	verify_webhook_secret: "asdf",	websocket_tcp_converter: true,});
import { CodeBlock, mantleCode } from "@ngrok/mantle/code-block"; <CodeBlock.Root>	<CodeBlock.Header>		<CodeBlock.Icon preset="file" />		<CodeBlock.Title>…</CodeBlock.Title>	</CodeBlock.Header>	<CodeBlock.Body>		<CodeBlock.CopyButton />		<CodeBlock.Code value={mantleCode("javascript")`…`} />	</CodeBlock.Body>	<CodeBlock.ExpanderButton /></CodeBlock.Root>;

Examples

Single line with a header

Many code blocks will be single line command line prompts and should be able to render with a header and copy button. This makes it absolutely clear that this example is a command line prompt and not a code sample.

Command Line

sudo unzip ~/Downloads/ngrok-v3-stable-darwin.zip -d /usr/local/bin
<CodeBlock.Root>	<CodeBlock.Header>		<CodeBlock.Icon preset="cli" />		<CodeBlock.Title>Command Line</CodeBlock.Title>	</CodeBlock.Header>	<CodeBlock.Body>		<CodeBlock.CopyButton />		<CodeBlock.Code			value={mantleCode(				"bash",			)`sudo unzip ~/Downloads/ngrok-v3-stable-darwin.zip -d /usr/local/bin`}		/>	</CodeBlock.Body></CodeBlock.Root>

Horizontal scrolling

This example is included to demonstrate that code blocks can scroll horizontally if the content is too wide. Mantle attempts to normalize scrollbar styling across browsers and platforms.

When line numbers are shown, the gutter (numbers and fold toggles) stays pinned to the left edge while the code scrolls under it.

ngrok-example.js

const http = require('http');const ngrok = require("@ngrok/ngrok");const server = http.createServer((req, res) => {	res.writeHead(200);	res.end("Hello!");	setTimeout(() => {		Promise.resolve().then(() => {			console.log("url:", server.tunnel.url());		});	}, timeout);});// Consumes authtoken from env automaticallyngrok.listen(server).then(() => {	console.log("url:", server.tunnel.url());});// really long line here that should wrap around and stuff Officia ipsum sint eu labore esse deserunt aliqua quis irure.
<CodeBlock.Root>	<CodeBlock.Header>		<CodeBlock.Icon preset="file" />		<CodeBlock.Title>ngrok-example.js</CodeBlock.Title>	</CodeBlock.Header>	<CodeBlock.Body>		<CodeBlock.CopyButton />		<CodeBlock.Code			value={mantleCode("javascript")`				const http = require('http');				const ngrok = require("@ngrok/ngrok");				const server = http.createServer((req, res) => {					res.writeHead(200);					res.end("Hello!");					setTimeout(() => {						Promise.resolve().then(() => {							console.log("url:", server.tunnel.url());						});					}, timeout);				});				// Consumes authtoken from env automatically				ngrok.listen(server).then(() => {					console.log("url:", server.tunnel.url());				});				// really long line here that should wrap around and stuff Officia ipsum sint eu labore esse deserunt aliqua quis irure.			`}		/>	</CodeBlock.Body></CodeBlock.Root>

No header or copy button

This is the most simple example of our code block component. While very useful, the copy button is optional. It is also perfectly acceptable to render a code block without a header, especially if the surrounding content carries the context or the code block is self-explanatory eg. "In your index.js file, paste the following:".

const http = require('http');const ngrok = require("@ngrok/ngrok");const server = http.createServer((req, res) => {	res.writeHead(200);	res.end("Hello!");});// Consumes authtoken from env automaticallyngrok.listen(server).then(() => {	console.log("url:", server.tunnel.url());});
<CodeBlock.Root>	<CodeBlock.Body>		<CodeBlock.Code			value={mantleCode("javascript")`				const http = require('http');				const ngrok = require("@ngrok/ngrok");				const server = http.createServer((req, res) => {					res.writeHead(200);					res.end("Hello!");				});				ngrok.listen(server).then(() => {					console.log("url:", server.tunnel.url());				});			`}		/>	</CodeBlock.Body></CodeBlock.Root>

Single line with horizontal scrolling

This example is included to show the interaction between the copy button and horizontal scrolling on a single verbose terminal command.

ffmpeg -i multichannel.mxf -map 0:v:0 -map 0:a:0 -map 0:a:0 -c:a:0 ac3 -b:a:0 640k -ac:a:1 2 -c:a:1 aac -b:2 128k out.mp4
<CodeBlock.Root>	<CodeBlock.Body>		<CodeBlock.CopyButton />		<CodeBlock.Code			value={mantleCode(				"bash",			)`ffmpeg -i multichannel.mxf -map 0:v:0 -map 0:a:0 -map 0:a:0 -c:a:0 ac3 -b:a:0 640k -ac:a:1 2 -c:a:1 aac -b:2 128k out.mp4`}		/>	</CodeBlock.Body></CodeBlock.Root>

Server-rendered syntax highlighting

The CodeBlock supports server-rendered syntax highlighting for dynamic or user-supplied code. Use createMantleCodeBlockValue() to construct a value from server-highlighted HTML and pass it to CodeBlock.Code. This is useful when the code to highlight isn't known at build time (for example, user input or API responses).

import {	CodeBlock,	createMantleCodeBlockValue,	type MantleCodeBlockValue,} from "@ngrok/mantle/code-block"; // Fetch highlighted HTML from your serverconst response = await fetch("/api/shiki-highlight", {	method: "POST",	headers: { "Content-Type": "application/json" },	body: JSON.stringify({ code, language: "typescript", showLineNumbers: true }),});const data = await response.json(); // Create a MantleCodeBlockValue from the server responseconst value: MantleCodeBlockValue = createMantleCodeBlockValue({	code: data.code,	language: "typescript",	preHtml: data.html,	showLineNumbers: data.showLineNumbers,	highlightLines: data.highlightLines,	lineNumberStart: data.lineNumberStart,}); <CodeBlock.Root>	<CodeBlock.Body>		<CodeBlock.CopyButton />		<CodeBlock.Code value={value} />	</CodeBlock.Body></CodeBlock.Root>;

Highlight microservice (for Go / non-Node.js backends)

For frontends backed by non-Node.js services (Go, Python, etc.), deploy a small Node.js highlight service as a sidecar or shared microservice. This gives you the same server-rendered highlighting without shipping Shiki to the browser; the frontend calls the API with fetch.

Create a highlight server using createMantleServerSyntaxHighlighter:

// highlight-service.ts — deploy as a sidecar alongside your Go serviceimport { createServer } from "node:http";import { createMantleServerSyntaxHighlighter } from "@ngrok/mantle-server-syntax-highlighter"; const highlighter = createMantleServerSyntaxHighlighter(); createServer(async (req, res) => {	// Handle CORS	res.setHeader("Access-Control-Allow-Origin", "*");	res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");	res.setHeader("Access-Control-Allow-Headers", "Content-Type");	if (req.method === "OPTIONS") {		res.writeHead(204).end();		return;	} 	const chunks: Buffer[] = [];	for await (const chunk of req) chunks.push(chunk);	const { code, language, showLineNumbers, highlightLines, lineNumberStart } = JSON.parse(		Buffer.concat(chunks).toString(),	); 	const result = await highlighter.highlight({		code,		language,		showLineNumbers,		highlightLines,		lineNumberStart,	});	res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(result));}).listen(4444, () => console.log("Highlight server on :4444"));

Then use it from any frontend the same way as the server-rendered example above, pointing at the highlight service URL:

const response = await fetch("http://localhost:4444", {	method: "POST",	headers: { "Content-Type": "application/json" },	body: JSON.stringify({ code, language: "json", showLineNumbers: true }),});const data = await response.json(); const value = createMantleCodeBlockValue({	code: data.code,	language: data.language,	preHtml: data.html,	showLineNumbers: data.showLineNumbers,});

Foldable code blocks

Code blocks automatically render editor-style fold carets in the gutter for any block whose language has a folding strategy: AST-based (JS/TS/JSX/TSX, HTML, XML), raw-source (JSON, CSS), bracket-paired (Go, Java, C#, Rust, Terraform), indentation-based (Python, YAML), keyword-paired (Bash/Shell), or bracket + keyword (Ruby). Click the caret (or focus and press Enter / Space) to collapse the inner lines; the opener and closer lines stay visible, and a ⋯ placeholder marks the collapsed region.

Folding is computed before the block reaches the browser, and the entire interaction runs through a single delegated click handler per code block: no per-line listeners and no React re-renders on toggle. This keeps fold/unfold cheap even for code blobs with thousands of lines.

The mantleCode() Vite transform, MDX code fences, and createMantleServerSyntaxHighlighter() use the full server-side folding dispatcher. The lower-level computeFoldRanges({ language, tokens }) export from @ngrok/mantle/highlight-utils is intentionally token-only for custom integrations; it covers bracket, indentation, and tag folds only. Use @ngrok/mantle-server-syntax-highlighter for AST, raw-source, or keyword strategies.

For one example per supported language plus a list of caveats, see folding by language.

package.json

{  "name": "@ngrok/mantle",  "version": "0.71.0",  "scripts": {    "build": "tsdown",    "test": "vitest run",    "typecheck": "tsc"  },  "dependencies": {    "@radix-ui/react-tabs": "1.1.13",    "clsx": "2.1.1",    "tailwind-merge": "3.5.0"  },  "pnpm": {    "onlyBuiltDependencies": [      "esbuild",      "@tailwindcss/oxide"    ]  }}
import { CodeBlock, mantleCode } from "@ngrok/mantle/code-block"; <CodeBlock.Root>	<CodeBlock.Header>		<CodeBlock.Icon preset="file" />		<CodeBlock.Title>package.json</CodeBlock.Title>	</CodeBlock.Header>	<CodeBlock.Body>		<CodeBlock.CopyButton />		<CodeBlock.Code			value={mantleCode("json")`				{					"name": "@ngrok/mantle",					"scripts": {						"build": "tsdown",						"test": "vitest run"					},					"pnpm": {						"onlyBuiltDependencies": [							"esbuild",							"@tailwindcss/oxide"						]					}				}			`}		/>	</CodeBlock.Body></CodeBlock.Root>;

Highlighting runtime JSON on the client

mantleCode() highlights at build time, so it can't highlight a value you only have at runtime: an API response, a table row's underlying record, anything fetched or computed in the browser. For that, use jsonCodeBlockValue(value): it serializes the value with JSON.stringify (2-space indent), tokenizes it, and returns a MantleCodeBlockValue you pass straight to CodeBlock.Code, entirely on the client, with no Shiki runtime, grammar, or WASM shipped to the browser (~1 KB). Because Mantle highlights with a CSS-variables theme the colors already live in CSS, so only tokenization happens at runtime; the output is byte-for-byte identical to a server/build-time highlighted block and adapts to light/dark themes for free.

Multi-line objects and arrays are collapsible by default (the same fold runtime as every other JSON block), so pass { foldable: false } for a flat panel, or { showLineNumbers: true } for a gutter. Serialization is safe and never throws: BigInt renders as its decimal string, circular/repeated references collapse to "[Circular]", and values that serialize to undefined render as an empty string. It's ideal for inspecting a record inside a DataTable.ExpandedRow.

{  "id": "evt_31kPq9aZ",  "type": "endpoint.created",  "created_at": "2025-06-18T17:42:10Z",  "endpoint": {    "id": "ep_2hYqLm4",    "region": "us-east-1",    "url": "https://example.ngrok.app",    "bindings": [      "public",      "internal"    ]  },  "metadata": {    "source": "api",    "retries": 0,    "ok": true  }}
import { CodeBlock, jsonCodeBlockValue } from "@ngrok/mantle/code-block"; // `record` is any runtime value — an API response, a table row's object, etc.<CodeBlock.Root>	<CodeBlock.Body>		<CodeBlock.CopyButton />		<CodeBlock.Code value={jsonCodeBlockValue(record)} />	</CodeBlock.Body></CodeBlock.Root>; // Opt out of folding, or show a line-number gutter:jsonCodeBlockValue(record, { foldable: false });jsonCodeBlockValue(record, { showLineNumbers: true });

For lower-level control, jsonToShikiHtml(code) returns only the Shiki-identical inner HTML for a <code> element (no line-number or fold decoration); jsonCodeBlockValue builds on it.

Tabbed code block

Use CodeBlock.TabList, CodeBlock.TabTrigger, and CodeBlock.TabContent to create a tabbed code block. Pass defaultTab (or activeTab / onActiveTabChange for controlled mode) to CodeBlock.Root, place the tab triggers in the CodeBlock.Header, and wrap each CodeBlock.Code in a CodeBlock.TabContent. The copy button automatically copies whichever code is currently displayed.

on_http_request:  - expressions:      # conditions are CEL expressions, see https://cel.dev      - req.url.path.startsWith('/api')    actions:      - type: forward-internal        config:          url: https://api.internal   # route dynamically based on a header using CEL interpolation  - actions:      - type: forward-internal        config:          url: "https://${req.headers('X-Custom-Header')}.internal"
import { CodeBlock, mantleCode } from "@ngrok/mantle/code-block"; const policyYml = mantleCode("yaml")`…`;const policyJson = mantleCode("json")`…`; <CodeBlock.Root defaultTab="yml">	<CodeBlock.Header>		<CodeBlock.TabList>			<CodeBlock.TabTrigger value="yml">policy.yml</CodeBlock.TabTrigger>			<CodeBlock.TabTrigger value="json">policy.json</CodeBlock.TabTrigger>		</CodeBlock.TabList>	</CodeBlock.Header>	<CodeBlock.Body>		<CodeBlock.CopyButton />		<CodeBlock.TabContent value="yml">			<CodeBlock.Code value={policyYml} />		</CodeBlock.TabContent>		<CodeBlock.TabContent value="json">			<CodeBlock.Code value={policyJson} />		</CodeBlock.TabContent>	</CodeBlock.Body></CodeBlock.Root>;

When the tabs are too wide for the header, CodeBlock.TabList scrolls horizontally with a scroll-fade-x edge fade instead of wrapping onto a second row (no extra wiring required).

console.log("Hello, ngrok!");

Reusing fragments with nested interpolation

A mantleCode value can be embedded into another mantleCode template by interpolating its .code property. When the Vite plugin recognizes the reference, it inlines the fragment's source at build time, so the embedded code is highlighted line-for-line in its host language (correct line numbers, folding, and copy text) with no runtime substitution cost.

This is ideal for sharing a Traffic Policy or config snippet across multiple SDK examples without duplicating it. The fragment can live at the top level of the same module, or be imported from another module:

var policy = """on_http_request:	- actions:			- type: oauth				config:					provider: google"""; var agent = Session.withAuthtoken(authtoken).connect();var listener = agent.listen(policy);
// shared-policies.ts — author the reusable policy once.import { mantleCode } from "@ngrok/mantle/code-block"; export const oauthPolicyFragment = mantleCode("yaml")`on_http_request:	- actions:			- type: oauth				config:					provider: google`;
// listener-demo.tsx — embed it into a Java text block.import { CodeBlock, mantleCode } from "@ngrok/mantle/code-block";import { oauthPolicyFragment } from "./shared-policies"; // `${oauthPolicyFragment.code}` is resolved and inlined at build time.const javaSdkSnippet = mantleCode("java")`var policy = """${oauthPolicyFragment.code}"""; var listener = agent.listen(policy);`;

The fragment must be a top-level mantleCode const referenced as .code: either declared earlier in the same module, or a named import of a directly-exported top-level const from another module. Nested fragments are resolved recursively. Interpolations that can't be resolved statically (dynamic values, re-exports, bare identifiers without .code) fall back to runtime placeholder substitution, and an unresolved imported .code reference emits a build warning. The embedded source is highlighted with the outer language's grammar (e.g. the YAML above is highlighted as Java text-block content, not as YAML).

mantleCode() options

The mantleCode() tagged template accepts an optional second argument with the following options:

  • showLineNumbers: Whether to show line numbers. Defaults to true for most languages, but false for single-line shell snippets (bash, sh, shell). Pass false to hide them or true to force them on.
  • highlightLines: An array of line numbers or ranges (e.g. [2, "4-5"]) to visually highlight.
  • lineNumberStart: The starting line number when line numbers are displayed. Defaults to 1.
  • indentation: Override the default indentation style ("tabs" or "spaces").

all-options.ts

import { serve } from "bun"; const server = serve({  port: 3000,  fetch(req) {    return new Response("Hello from Bun!");  },}); console.log(`Listening on ${server.url}`);
import { CodeBlock, mantleCode } from "@ngrok/mantle/code-block"; <CodeBlock.Root>	<CodeBlock.Header>		<CodeBlock.Icon preset="file" />		<CodeBlock.Title>all-options.ts</CodeBlock.Title>	</CodeBlock.Header>	<CodeBlock.Body>		<CodeBlock.CopyButton />		<CodeBlock.Code			value={mantleCode("typescript", {				showLineNumbers: false,				highlightLines: [11, "13-14"],				lineNumberStart: 10,				indentation: "spaces",			})`				import { serve } from "bun"; 				const server = serve({					port: 3000,					fetch(req) {						return new Response("Hello from Bun!");					},				}); 				console.log(\`Listening on \${server.url}\`);			`}		/>	</CodeBlock.Body></CodeBlock.Root>;

Overriding defaults

The mantleCode() options let you customize how code is displayed. By default, line numbers are shown starting at 1 and indentation is inferred from the language. You can also specify highlighted lines per code block. The first example below uses the defaults (YAML auto-detects space indentation), while the second overrides all options: custom indentation, line number start, highlighted lines, and visible line numbers.

traffic-policy.yaml

# yaml indentation MUST use spaces (we infer this for you)on_http_request:  actions:    type: custom-response    config:      status_code: 200      content: Hello, World!

ngrok-example.js (all options overridden)

// by default, mantle decides that javascript uses tabs,// but this example uses spaces for indentationconst http = require('http');const ngrok = require("@ngrok/ngrok");const server = http.createServer((req, res) => {  res.writeHead(200);  res.end("Hello!");  setTimeout(() => {    Promise.resolve().then(() => {      console.log("url:", server.tunnel.url());    });  }, timeout);});// Consumes authtoken from env automaticallyngrok.listen(server).then(() => {  console.log("url:", server.tunnel.url());});
{	/* yaml auto-detects space indentation, all other defaults apply */}<CodeBlock.Code	value={mantleCode("yaml")`		on_http_request:			actions:				type: custom-response				config:					status_code: 200					content: Hello, World!	`}/>; {	/* override all mantleCode() options */}<CodeBlock.Code	value={mantleCode("javascript", {		indentation: "spaces",		showLineNumbers: true,		highlightLines: [42, "46-48"],		lineNumberStart: 42,	})`		const http = require('http');		const ngrok = require("@ngrok/ngrok");		const server = http.createServer((req, res) => {			res.writeHead(200);			res.end("Hello!");		});	`}/>;

Composition

Compose the parts of a CodeBlock together to build your own:

# StandardCodeBlock.Root├── CodeBlock.Header│   ├── CodeBlock.Icon│   └── CodeBlock.Title├── CodeBlock.Body│   ├── CodeBlock.CopyButton│   └── CodeBlock.Code└── CodeBlock.ExpanderButton# TabbedCodeBlock.Root├── CodeBlock.Header│   └── CodeBlock.TabList│       └── CodeBlock.TabTrigger├── CodeBlock.Body│   ├── CodeBlock.CopyButton│   └── CodeBlock.TabContent│       └── CodeBlock.Code└── CodeBlock.ExpanderButton

Polymorphism

CodeBlock.Root, CodeBlock.Header, CodeBlock.Title, CodeBlock.Body, and CodeBlock.CopyButton accept asChild. When true the part renders its single child instead of its default element, forwarding class names, data-* attributes, and ref onto it.

Reach for it when Root should be a semantic <figure> or <section>, when Title needs a different heading level than the default <h3>, or when Header and Body should be <header> and <section>. CopyButton has no children of its own, so it is typed SelfClosingWithAsChild: <CodeBlock.CopyButton asChild> requires exactly one child to clone, and forbids children otherwise.

The remaining parts do not accept asChild. CodeBlock.Code owns the highlighted markup it renders, and CodeBlock.TabList, CodeBlock.TabTrigger, and CodeBlock.TabContent each explicitly omit the prop because their behavior and mounting are driven by the active tab. CodeBlock.Icon takes its element through the svg prop instead. CodeBlock.ExpanderButton renders its own "Show more" / "Show less" label and caret icon, so there is no single child to clone; style it with className.

Translation

CodeBlock.Code and CodeBlock.Title carry translate="no", so a browser translation engine skips the code and the filename. A translated CLI flag, env var, or YAML key is wrong, and the reader copies it anyway. A translated example.ts names a file that does not exist.

The attribute also protects the render itself: Google Translate replaces each text node with a <font> wrapper, which detaches the text nodes React holds, so React either writes updates into a node the DOM no longer shows, or throws NotFoundError from removeChild and tears down the root.

The two parts differ on whether a call site may turn the guard off. CodeBlock.Code omits the translate prop from its type, because a block of code is never translatable. CodeBlock.Title keeps it, because the slot takes arbitrary children; pass translate="yes" for a title that is prose rather than a filename.

Accessibility

  • CodeBlock.CopyButton is an icon button named by label (default "Copy code"). After a copy, an always-mounted role="status" element beside the button announces "Copied", so a screen-reader user hears the result and not only the icon swap.
  • CodeBlock.ExpanderButton carries aria-expanded and aria-controls that points at the <pre>. The <pre> itself carries no ARIA state; data-state="collapsed" | "expanded" drives the styling.
  • Fold toggles are real <button> elements named "Toggle code folding" with aria-expanded. Enter and Space toggle them. The folded marker is aria-hidden.
  • Line numbers are aria-hidden, so a screen reader reads the code, not "1 const x".
  • CodeBlock.Code and CodeBlock.Title carry translate="no"; see Translation.

API Reference

The CodeBlock renders and applies syntax highlighting to blocks of code and is composed of several sub-components.

CodeBlock.Root

Root container for all CodeBlock sub-components. For tabbed code blocks, pass defaultTab (uncontrolled) or activeTab / onActiveTabChange (controlled) to enable tab switching.

All props from standard HTML div attributes, plus:

PropTypeDefaultDescription
asChild?booleanfalseUse the asChild prop to compose the CodeBlock styling and functionality onto alternative element types or your own React components.
defaultTab?stringThe default active tab value (uncontrolled). Only relevant when using TabList / TabContent.
activeTab?stringThe controlled active tab value. Only relevant when using TabList / TabContent.
onActiveTabChange?(value: string) => voidCallback fired when the active tab changes. Only relevant when using TabList / TabContent.

CodeBlock.Body

The body of the CodeBlock. This is where the CodeBlock.Code and optional CodeBlock.CopyButton are rendered as direct children.

All props from standard HTML div attributes, plus:

PropTypeDefaultDescription
asChild?booleanfalseUse the asChild prop to compose the CodeBlock.Body styling and functionality onto alternative element types or your own React components.

CodeBlock.Code

The CodeBlock content. This is where the code is rendered with pre-highlighted Shiki HTML.

The <pre> always carries translate="no".

All props from standard HTML pre attributes except translate, plus:

PropTypeDefaultDescription
valueMantleCodeBlockValueThe code value produced by mantleCode("lang") tagged template or createMantleCodeBlockValue().
Data AttributeValueDescription
data-state"collapsed" | "expanded"Present when a CodeBlock.ExpanderButton is composed. Drives the collapsed max height.
data-highlighted"true" | "false"Whether the value carried pre-rendered Shiki HTML.
data-langthe language idThe value's language.
data-mantle-line-numbers"true" | "false"Whether the gutter shows line numbers.
data-mantle-line-number-starta number as a stringThe first line number.
data-mantle-highlight-linescomma-separated line numbersPresent when lines are highlighted.

Inside the highlighted HTML, data-slot="line-number", data-slot="fold-toggle", and data-slot="fold-ellipsis" mark the gutter number, the fold button, and the folded marker.

CSS VariableDefaultDescription
--mantle-line-number-start1The CSS counter start for the line-number gutter.

CodeBlock.Header

An optional header slot of the CodeBlock. This is where things like the CodeBlock.Icon and CodeBlock.Title are rendered.

All props from standard HTML div attributes, plus:

PropTypeDefaultDescription
asChild?booleanfalseUse the asChild prop to compose the CodeBlock.Header styling and functionality onto alternative element types or your own React components.

CodeBlock.Title

The (optional) title of a CodeBlock. Default renders as an h3 element; use asChild to render something else.

The heading carries translate="no" by default, because the title usually names a file.

All props from standard HTML h3 attributes, plus:

PropTypeDefaultDescription
asChild?booleanfalseUse the asChild prop to compose the CodeBlock.Title styling and functionality onto alternative element types or your own React components.
translate?"yes" | "no""no"Whether a browser translation engine may translate the title. Pass "yes" for a title that is prose rather than a filename.

CodeBlock.CopyButton

The optional copy button. Render it as a child of CodeBlock.Body. It copies the raw source of the mounted CodeBlock.Code to the clipboard — in a tabbed block, the active tab's code.

All props from standard HTML button attributes, plus:

PropTypeDefaultDescription
label?string"Copy code"The accessible name for the copy button, rendered as its aria-label.
onCopy?(value: string) => voidCallback fired when the copy button is clicked, passes the copied text as an argument.
onCopyError?(error: unknown) => voidCallback fired when an error occurs during copying.
asChild?booleanfalseUse the asChild prop to compose the CodeBlock.CopyButton styling and functionality onto alternative element types or your own React components.

After a copy, an always-mounted role="status" element inside the part announces "Copied".

CodeBlock.ExpanderButton

The (optional) expander button of the CodeBlock. Render this as a child of the CodeBlock.Root to allow users to expand/collapse the code block contents. It carries aria-expanded and aria-controls that points at the <pre>. It does not take asChild: it renders its own label and caret, so style it with className.

All props from standard HTML button attributes except children, aria-controls, and aria-expanded.

CodeBlock.Icon

A small icon that represents the type of code block being displayed, rendered as an SVG next to the code block title in the code block header. You can pass in a custom SVG component or use one of the presets (you can exclusively pass one of svg or preset).

All props from Icon, plus:

PropTypeDefaultDescription
svg?ReactNodeA custom icon to display in the code block header. You can exclusively pass one of svg or preset
preset?"cli" | "file" | "traffic-policy"A preset icon to display in the code block header. You can exclusively pass one of svg or preset

CodeBlock.TabList

A tab list for the CodeBlock header. Renders pill-styled tab triggers that switch which code is displayed. Place this inside CodeBlock.Header and pair with CodeBlock.TabContent in CodeBlock.Body. CodeBlock.Root owns the tab state via defaultTab / activeTab / onActiveTabChange.

All props from standard HTML div attributes.

CodeBlock.TabTrigger

A pill-styled tab trigger for the CodeBlock header. Must be rendered within a CodeBlock.TabList.

All props from standard HTML button attributes, plus:

PropTypeDefaultDescription
valuestringThe tab value this trigger activates. Must match the value of a corresponding TabContent.

CodeBlock.TabContent

Conditionally renders its children when the associated tab is active. Pair with CodeBlock.TabList and CodeBlock.TabTrigger in the header, and set defaultTab / activeTab on CodeBlock.Root.

All props from standard HTML div attributes, plus:

PropTypeDefaultDescription
valuestringThe tab value this content is associated with. Only rendered when this matches the active tab.

jsonCodeBlockValue

Builds a MantleCodeBlockValue that renders any value as syntax-highlighted JSON entirely on the client — no Shiki runtime, build-time plugin, or server roundtrip (see Highlighting runtime JSON on the client). Pass the result to CodeBlock.Code's value prop. Serialization is safe and never throws: BigInt → decimal string, circular/repeated references → "[Circular]", and values that serialize to undefined → "".

jsonCodeBlockValue(value: unknown, options?: JsonCodeBlockValueOptions): MantleCodeBlockValue
OptionTypeDefaultDescription
foldable?booleantrueGive multi-line objects/arrays collapsible fold toggles (the same fold runtime as every other JSON block).
showLineNumbers?booleanfalseRender a line-number gutter. Detail panels usually read better without one.

Byte-for-byte parity with server/build-time Shiki output is guaranteed for canonical JSON.stringify shapes — which is exactly what this function produces.

jsonToShikiHtml

The lower-level primitive behind jsonCodeBlockValue. Tokenizes a JSON string into the same CSS-variable-colored <code> inner HTML that Mantle's server/build-time Shiki highlighter produces, without shipping Shiki to the browser. Returns an HTML string with no line-number or fold decoration — feed it through decorateHighlightedHtml for that, as jsonCodeBlockValue does. Reach for it only when you need the raw markup; most consumers want jsonCodeBlockValue.

jsonToShikiHtml(code: string): string