# @ngrok/mantle — Full Documentation
> Concatenated markdown for every page on https://mantle.ngrok.com. Each section is preceded by its canonical docs URL. JSX preview blocks (``) are dropped; code fences are preserved verbatim.
Docs index: https://mantle.ngrok.com/llms.txt
Component manifest: https://mantle.ngrok.com/api/components.json
Hooks manifest: https://mantle.ngrok.com/api/hooks.json
Utilities manifest: https://mantle.ngrok.com/api/utils.json
Package info: https://mantle.ngrok.com/api/package.json
Changelog (structured): https://mantle.ngrok.com/api/changelog.json
Search index: https://mantle.ngrok.com/api/search-index.json
Schemas: https://mantle.ngrok.com/api/schema.json
---
---
title: Accessibility
description: How mantle approaches accessibility, the keyboard and ARIA contracts each component honors, and how to build accessible UIs on top of it.
---
# Accessibility
Mantle is built so the accessible thing and the easy thing are the same thing. Components render real semantic HTML, lean on battle-tested primitives ([Radix](https://www.radix-ui.com), [Ariakit](https://ariakit.org), [Headless UI](https://headlessui.com)) for complex interaction patterns, and ship with the keyboard, focus, and ARIA behavior already wired up. This page collects the cross-cutting guidance that doesn't fit on any single component page.
## Principles
- **Semantic HTML first.** A [`Button`](/components/button) is a `
And this icon renders at the end:{" "}
} iconPlacement="end">
ngrok dashboard
!
```
## API Reference
### Anchor
All props from [a](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attributes), plus:
| Prop | Type | Default | Description |
| ---------------- | -------------------- | --------- | ------------------------------------------------------------------------------------------------------------------- |
| `icon?` | `ReactNode` | | An icon to render inside the anchor. |
| `iconPlacement?` | `"start"` \| `"end"` | `"start"` | The side that the icon will render on, if one is present. |
| `asChild?` | `boolean` | `false` | Use the `asChild` prop to compose the `Anchor` styling onto alternative element types or your own React components. |
---
---
title: Badge
description: A non-interactive label used to highlight short, scannable information — a status, a category tag, or a count — in the smallest possible footprint.
---
# Badge
A non-interactive label used to highlight short, scannable information — a status, a category tag, or a count — in the smallest possible footprint.
## When to use
- Status indicators: `Succeeded`, `Failed`, `Pending`, `Beta`.
- Category or tag chips alongside list items, table rows, or cards.
- Counts (e.g. `12 new`) when paired with brief context.
## When not to use
- For interactive UI. Badges are not buttons or links — use [`Button`](/components/button) or [`Anchor`](/components/anchor) (optionally with `asChild` styling) instead.
- For long-form text. Keep labels to one or two short words.
- As the sole signal of meaning. Pair color with a label or icon so the distinction works without color (color blindness, monochrome themes).
## Choosing a color
Prefer functional colors (`success`, `warning`, `danger`, `info`, `accent`, `neutral`) for status meaning so theming stays coherent. Reach for named hues only when the badge's semantic role isn't already covered.
```tsx
import { Badge } from "@ngrok/mantle/badge";
import { GlobeHemisphereWestIcon } from "@phosphor-icons/react/GlobeHemisphereWest";
Muted neutral
}>
Muted neutral
```
## Polymorphism
When you want to render *something else* as a `Badge`, you can use the `asChild` prop to compose. This is useful when you want to splat the `Badge` styling onto a `react-router` `Link`.
```tsx
import { Badge } from "@ngrok/mantle/badge";
import { GlobeHemisphereWestIcon } from "@phosphor-icons/react/GlobeHemisphereWest";
import { Link, href } from "react-router";
}>
See our colors!
;
```
## API Reference
### Badge
All props from [span](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span#attributes), plus:
| Prop | Type | Default | Description |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ |
| `appearance` | `"muted"` | | Defines the visual style of the `Badge`. Currently only supports the `muted` variant. |
| `asChild?` | `boolean` | `false` | Use the `asChild` prop to compose the `Badge` styling onto alternative element types or your own React components. |
| `color?` | `"neutral"` \| `"danger"` \| `"important"` \| `"info"` \| `"success"` \| `"warning"` \| `"blue"` \| `"cyan"` \| `"fuchsia"` \| `"gray"` \| `"green"` \| `"indigo"` \| `"lime"` \| `"orange"` \| `"pink"` \| `"purple"` \| `"red"` \| `"teal"` \| `"yellow"` | `"neutral"` | The color variant of the `Badge`. Supports all [named colors](/base/colors), both functional and from the palette. |
| `icon?` | `ReactNode` | | An icon to render inside the badge. Will be automatically sized for you. |
---
---
title: BrowserOnly
description: A wrapper component that ensures its children only render in the browser, after hydration has completed.
---
# BrowserOnly
A wrapper component that ensures its children only render in the browser, after hydration has completed. Useful for components that rely on browser-only APIs like `window`, `document`, `localStorage`, or media queries.
```tsx
import { BrowserOnly } from "@ngrok/mantle/browser-only";
}>
{() =>
This only renders in the browser after hydration!
}
Loading...}>
{() => (
Browser-only content with window dimensions:
Width: {window.innerWidth}px
Height: {window.innerHeight}px
)}
```
## API Reference
### BrowserOnly
| Prop | Type | Default | Description |
| ----------- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `children` | `() => ReactNode` | | Children must be a render function so that evaluation is deferred until after hydration has occurred. |
| `fallback?` | `ReactNode` | `null` | Optional fallback to render on the server (and during hydration) before the client-only children are mounted. Ideally, this should be the same dimensions as the eventual children to avoid layout shift. |
---
---
title: Button
description: Initiates an action, such as completing a task or submitting information.
---
# Button
Initiates an action, such as completing a task or submitting information.
```tsx
import { Button } from "@ngrok/mantle/button";
OutlinedFilledGhostLinkOutlinedFilledGhostLinkOutlinedFilledGhostLink
```
## Type
Unlike the native `` element — which defaults to `type="submit"` — a mantle `Button` defaults to **`type="button"`**. This matches the wider React ecosystem (Radix, shadcn, MUI, …) and stops a button from accidentally submitting a surrounding `
```
## isLoading
`isLoading` determines whether or not the button is in a loading state, default `false`. Setting `isLoading` will replace any `icon` with a spinner, or add one if an icon wasn't given. It will also disable user interaction with the button and set `aria-disabled`.
```tsx
import { Button } from "@ngrok/mantle/button";
import { FireIcon } from "@phosphor-icons/react/Fire";
No Icon + Idle}>Icon Start + Idle
} iconPlacement="end">
Icon End + Idle
No Icon + isLoading} isLoading>
Icon Start + isLoading
} iconPlacement="end" isLoading>
Icon End + isLoading
```
## Polymorphism
When you want to render *something else* as a `Button`, you can use the `asChild` prop to compose. This is useful when you want to splat the `Button` styling onto a `react-router` `Link`. Keep in mind that when you use `asChild` the `type` prop will **NOT** be passed to the child component.
```tsx
import { Button } from "@ngrok/mantle/button";
import { FireIcon } from "@phosphor-icons/react/Fire";
import { Link, href } from "react-router";
} asChild>
See our colors!
;
```
## API Reference
### Button
All props from [button](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes), plus:
| Prop | Type | Default | Description |
| ---------------- | --------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `appearance?` | `"ghost"` \| `"filled"` \| `"outlined"` \| `"link"` | `"outlined"` | Defines the visual style of the `Button`. |
| `asChild?` | `boolean` | `false` | Use the `asChild` prop to compose the `Button` styling onto alternative element types or your own React components. |
| `icon?` | `ReactNode` | | An icon to render inside the button. When `isLoading` is `true`, the icon will automatically be replaced with a spinner. |
| `iconPlacement?` | `"start"` \| `"end"` | `"start"` | The side that the icon will render on, if one is present. When `isLoading` is `true`, the loading spinner will also render on this side. |
| `isLoading?` | `boolean` | `false` | Determines whether or not the button is in a loading state. Setting `isLoading` will replace any `icon` with a spinner, or add one if an icon wasn't given. It will also disable user interaction with the button and set `aria-disabled`. |
| `priority?` | `"default"` \| `"danger"` \| `"neutral"` | `"default"` | Indicates the importance or impact level of the button, affecting its color and styling to communicate its purpose to the user. |
| `type?` | `"button"` \| `"reset"` \| `"submit"` | `"button"` | The behavior of the `Button` when activated. Defaults to `"button"`, so a `Button` inside a `
{(field) => (
field.handleChange(event.target.checked)}
/>
Accept terms and conditions
)}
Submit
);
}
```
## Select-all helper
`selectAllChecked` resolves the tri-state `checked` value for a "select all" checkbox from the current selection counts — `true` when everything is selected, `"indeterminate"` when only some is, and `false` when nothing is. Use it to keep the header checkbox of a multi-select list correct without hand-rolling the tri-state branch. It pairs directly with a [`DataTable`](/components/data-table#row-selection-with-checkboxes) row-selection column:
```tsx
import { Checkbox, selectAllChecked } from "@ngrok/mantle/checkbox";
table.toggleAllRowsSelected(event.target.checked)}
/>;
```
Returns `boolean | "indeterminate"` (a `CheckedState`), ready to pass straight to `Checkbox`'s `checked` prop. `allSelected` takes precedence, so it wins even if a caller also reports `someSelected`.
| Option | Type | Description |
| :------------- | :-------- | :--------------------------------------------------------- |
| `allSelected` | `boolean` | Whether every selectable item is currently selected. |
| `someSelected` | `boolean` | Whether some (but not necessarily all) items are selected. |
## API Reference
### Checkbox
All props from [input\[type="checkbox"\]](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox), plus:
| Prop | Type | Default | Description |
| ----------------- | -------------------------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `checked?` | `boolean` \| `"indeterminate"` | | Whether the checkbox is checked or not. Setting this to `"indeterminate"` will show the indeterminate state. This is useful for parent-child relationships, but requires manual, controlled state. |
| `defaultChecked?` | `boolean` \| `"indeterminate"` | | The checked state of the checkbox when it is initially rendered. Use when you do not need to control its checked state. |
| `validation?` | `"error"` \| `"success"` \| `"warning"` \| `false` \| `() => "error" \| "success" \| "warning" \| false` | | Use the `validation` prop to show a specific validation status. This will change the border and outline of the checkbox. The `false` type is useful with short-circuiting logic. Setting `validation` to `"error"` also sets `aria-invalid`. |
---
---
title: Code
description: Marks a short fragment of inline computer code — a function name, a variable, a CLI flag, a key.
---
# Code
Marks a short fragment of inline computer code — a function name, a variable, a CLI flag, a key. Renders a native `` element with mantle's monospace styling.
## When to use
- Inline within prose to identify code, file paths, env vars, or keys.
- Wrap technical terms that should visually stand apart from running text.
## When not to use
- For multi-line or syntax-highlighted blocks. Use [`CodeBlock`](/components/code-block) instead.
- For keyboard shortcuts. Use [`Kbd`](/components/kbd).
- For arbitrary monospace text that isn't code (use a plain monospace utility class).
```tsx
import { Code } from "@ngrok/mantle/code";
Use the console.log() function to debug your code.
;
```
## Polymorphism
Pass `asChild` to render `Code` styling on a different element — for example, a link wrapping a code-styled label.
```tsx
import { Anchor } from "@ngrok/mantle/anchor";
import { Code } from "@ngrok/mantle/code";
/api/components.json;
```
## API Reference
### Code
All props from [code](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code#attributes), plus:
| Prop | Type | Default | Description |
| ------------ | ----------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
| `asChild?` | `boolean` | `false` | Use the `asChild` prop to compose the `Code` styling onto alternative element types or your own React components. |
| `children` | `ReactNode` | | The content to be rendered inside the inline code element. |
| `className?` | `string` | | Additional CSS classes to apply to the inline code element. |
---
---
title: Code Block
description: Code blocks render and apply syntax highlighting to blocks of code using Shiki at build time.
---
# Code Block
Code blocks render and apply syntax highlighting to blocks of code. Syntax highlighting is performed at build time using [Shiki](https://shiki.style/) via the `mantleCodeBlockPlugins()` Vite plugin, 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.
```tsx
import { CodeBlock, mantleCode } from "@ngrok/mantle/code-block";
…;
```
## 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.
```tsx
Command Line
```
### 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.
```tsx
ngrok-example.js {
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.
`}
/>
```
### 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 context is provided in the surrounding content or the code block is self-explanatory eg. "In your index.js file, paste the following:".
```tsx
{
res.writeHead(200);
res.end("Hello!");
});
ngrok.listen(server).then(() => {
console.log("url:", server.tunnel.url());
});
`}
/>
```
### 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.
```tsx
```
### Server-Rendered Syntax Highlighting
The `CodeBlock` supports server-rendered syntax highlighting for dynamic or user-provided 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.
```tsx
import {
CodeBlock,
createMantleCodeBlockValue,
type MantleCodeBlockValue,
} from "@ngrok/mantle/code-block";
// Fetch highlighted HTML from your server
const 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 response
const value: MantleCodeBlockValue = createMantleCodeBlockValue({
code: data.code,
language: "typescript",
preHtml: data.html,
showLineNumbers: data.showLineNumbers,
highlightLines: data.highlightLines,
lineNumberStart: data.lineNumberStart,
});
;
```
### 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 just calls the API with `fetch`.
Create a highlight server using `createMantleServerSyntaxHighlighter`:
```ts
// highlight-service.ts — deploy as a sidecar alongside your Go service
import { 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](#server-rendered-syntax-highlighting) above, pointing at the highlight service URL:
```tsx
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), 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](/components/code-block/folding-by-language).
```tsx
import { CodeBlock, mantleCode } from "@ngrok/mantle/code-block";
package.json;
```
### 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`](/components/data-table#expandable-rows).
```tsx
import { CodeBlock, jsonCodeBlockValue } from "@ngrok/mantle/code-block";
// `record` is any runtime value — an API response, a table row's object, etc.
;
// 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 just the Shiki-identical inner HTML for a `` 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.
```tsx
import { CodeBlock, mantleCode } from "@ngrok/mantle/code-block";
const policyYml = mantleCode("yaml")`…`;
const policyJson = mantleCode("json")`…`;
policy.ymlpolicy.json;
```
### 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:
```tsx
// 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`;
```
```tsx
// 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. Note that the embedded source is highlighted using 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"`).
```tsx
import { CodeBlock, mantleCode } from "@ngrok/mantle/code-block";
all-options.ts;
```
### 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.
```tsx
{
/* yaml auto-detects space indentation, all other defaults apply */
}
;
{
/* override all mantleCode() options */
}
{
res.writeHead(200);
res.end("Hello!");
});
`}
/>;
```
## 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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div#attributes), plus:
| Prop | Type | Default | Description |
| :------------------- | :------------------------ | :------ | :--------------------------------------------------------------------------------------------------------------------------------------- |
| `asChild?` | `boolean` | `false` | Use the `asChild` prop to compose the `CodeBlock` styling and functionality onto alternative element types or your own React components. |
| `defaultTab?` | `string` | | The default active tab value (uncontrolled). Only relevant when using `TabList` / `TabContent`. |
| `activeTab?` | `string` | | The controlled active tab value. Only relevant when using `TabList` / `TabContent`. |
| `onActiveTabChange?` | `(value: string) => void` | | Callback 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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div#attributes), plus:
| Prop | Type | Default | Description |
| :--------- | :-------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------- |
| `asChild?` | `boolean` | `false` | Use 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.
All props from [standard HTML pre attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre#attributes), plus:
| Prop | Type | Default | Description |
| :------ | :--------------------- | :------ | :------------------------------------------------------------------------------------------------- |
| `value` | `MantleCodeBlockValue` | | The code value produced by `mantleCode("lang")` tagged template or `createMantleCodeBlockValue()`. |
### 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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div#attributes), plus:
| Prop | Type | Default | Description |
| :--------- | :-------- | :------ | :---------------------------------------------------------------------------------------------------------------------------------------------- |
| `asChild?` | `boolean` | `false` | Use 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.
All props from [standard HTML h3 attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements#attributes), plus:
| Prop | Type | Default | Description |
| :--------- | :-------- | :------ | :--------------------------------------------------------------------------------------------------------------------------------------------- |
| `asChild?` | `boolean` | `false` | Use the `asChild` prop to compose the `CodeBlock.Title` styling and functionality onto alternative element types or your own React components. |
### CodeBlock.CopyButton
The (optional) copy button of the `CodeBlock`. Render this as a child of the `CodeBlock.Body` to allow users to copy the code block contents to their clipboard.
All props from [standard HTML button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes), plus:
| Prop | Type | Default | Description |
| :------------- | :------------------------- | :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label?` | `string` | `"Copy code"` | The accessible label for the copy button. Visually hidden but announced to screen reader users. |
| `onCopy?` | `(value: string) => void` | | Callback fired when the copy button is clicked, passes the copied text as an argument. |
| `onCopyError?` | `(error: unknown) => void` | | Callback fired when an error occurs during copying. |
| `asChild?` | `boolean` | `false` | Use the `asChild` prop to compose the `CodeBlock.CopyButton` styling and functionality onto alternative element types or your own React components. |
### 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.
All props from [standard HTML button attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes), plus:
| Prop | Type | Default | Description |
| :--------- | :-------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `asChild?` | `boolean` | `false` | Use the `asChild` prop to compose the `CodeBlock.ExpanderButton` styling and functionality onto alternative element types or your own React components. |
### 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](/components/icon), plus:
| Prop | Type | Default | Description |
| :-------- | :---------------------------------------- | :------ | :--------------------------------------------------------------------------------------------------- |
| `svg?` | `ReactNode` | | A 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`. Tab state is managed by `CodeBlock.Root` via `defaultTab` / `activeTab` / `onActiveTabChange`.
All props from [standard HTML div attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes), plus:
| Prop | Type | Default | Description |
| :------ | :------- | :------ | :-------------------------------------------------------------------------------------------- |
| `value` | `string` | | The 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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div#attributes), plus:
| Prop | Type | Default | Description |
| :------ | :------- | :------ | :--------------------------------------------------------------------------------------------- |
| `value` | `string` | | The 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](#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` → `""`.
```ts
jsonCodeBlockValue(value: unknown, options?: JsonCodeBlockValueOptions): MantleCodeBlockValue
```
| Option | Type | Default | Description |
| :----------------- | :-------- | :------ | :--------------------------------------------------------------------------------------------------------- |
| `foldable?` | `boolean` | `true` | Give multi-line objects/arrays collapsible fold toggles (the same fold runtime as every other JSON block). |
| `showLineNumbers?` | `boolean` | `false` | Render 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 `` 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`.
```ts
jsonToShikiHtml(code: string): string
```
---
---
title: Code Block Folding by Language
description: One foldable code block per supported language, showing how each folding strategy behaves.
---
# Code Block Folding by Language
Mantle dispatches each supported language to the folding strategy that best fits its syntax. Most strategies run server- or build-side; only the toggle interaction runs in the browser.
- **AST-based (JS / TS / JSX / TSX)** — `oxc-parser` walks the AST and emits folds for block/class/switch bodies, object/array literals and patterns, JSX elements and fragments, multi-line template literals, and TypeScript constructs such as interfaces, type literals, enums, modules, and mapped types.
- **AST-based (HTML / XML)** — `parse5` tokenizes the source with `sourceCodeLocationInfo`, so void elements never open folds, self-closing XML tags are honored, and nested content inside `` folds normally. Multi-line opening tags fold their attribute list into the tag name.
- **Raw-source (CSS / JSON)** — single-pass, parser-quality delimiter matchers. CSS folds `{ … }`; JSON folds `{ … }` and `[ … ]`; both skip strings and escapes, and CSS also skips comments.
- **Indentation-based (Python / YAML)** — blocks are detected by leading whitespace; the opener line stays visible and child lines collapse.
- **Bracket-paired (Go / Rust / Java / C#)** — multi-line `{ … }` / `[ … ]` ranges fold via TextMate-scope-aware token walking; brackets inside strings, comments, or regular expressions are ignored.
- **Keyword-paired (Bash / Shell)** — opener / closer keyword pairs from VS Code's grammars: `if … fi`, `case … esac`, `for|while|until|select … do … done`, and brace-form function bodies. Same-line close markers are command-boundary aware, so variables or arguments named `done`, `fi`, or `esac` do not suppress valid folds.
- **Bracket + keyword (Ruby)** — combines bracket-paired folding (block-style `{ … }`, arrays) with Ruby's keyword pairs: `def`, `class`, `module`, `begin`, `do`, `if`, `case`, etc., all closing on `end`.
Plain text (`text`, `txt`, `plain`, `plaintext`) intentionally has no folds.
The examples on this page use the full server highlighter pipeline. If you're building custom highlighting with `@ngrok/mantle/highlight-utils`, `computeFoldRanges({ language, tokens })` is a lower-level token helper for bracket, indentation, and tag folds. Use `@ngrok/mantle-server-syntax-highlighter` when you need the AST, raw-source, or keyword strategies listed here.
← [Back to Code Block](/components/code-block)
## AST-based languages
### JavaScript
### TypeScript
### TSX
JSX elements (`…`), fragments (`<>…>`), and multi-line template literals all fold in addition to function bodies and object/array literals.
### JSX
### HTML
#### Multi-line opening tag
`parse5` emits `startTag.startLine`/`endLine` for multi-line opening tags, so the attribute list collapses into the tag name (matching VS Code).
### XML
XML-mode parsing routes through `parse5`'s SVG fragment context for XML-like (foreign-content) tokenizer rules — so `` and `` are correctly self-closing.
## Raw-source languages
### JSON
### CSS
## Indentation-based languages
### Python
### YAML
## Bracket-paired languages
### Go
### Java
### C\#
### Rust
## Keyword-paired languages
### Ruby
Ruby combines bracket folding (block-style `{ … }`, arrays) with keyword pairs — `def`, `class`, `module`, `begin`, `do`, `if`, `case`, etc., all closing on `end`.
### Bash / Shell
`if … fi`, `case … esac`, `for|while|until|select … do … done`, and brace-form function bodies all fold.
## Caveats
- **JSX / TSX self-closing elements** (``) get a toggle only when their attributes span multiple lines; collapsing hides the attribute lines while keeping the opening line and self-closing `/>` line visible.
- **Bracket strategy ignores `(…)`** — argument lists and tuple / parenthesized expressions never fold. This mirrors VS Code's default bracket folding.
- **Indentation strategy assumes consistent leading whitespace** within a block. Mixing tabs and spaces inside the same block can produce wrong spans.
- **Single-line spans never fold** — if a construct's opener and closer resolve to the same line, it emits no range. Inline objects/arrays inside a multi-line parent don't get their own toggle.
- **CSS folding is brace-only** — `@import url(…)` and other no-body at-rules don't have their own toggle (there's nothing to fold).
- **Shell same-line close detection is conservative** — line-leading closers and command-separated closers such as `; fi` suppress single-line folds, but ordinary data like `$done` or `echo done` does not.
---
---
title: Combobox
description: Fill in a React input field with autocomplete & autosuggest functionalities. Choose from a list of suggested values with full keyboard support.
---
# Combobox
Fill in a React input field with autocomplete & autosuggest functionalities. Choose from a list of suggested values with full keyboard support. This component is based on the [WAI-ARIA Combobox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) and is powered by the [ariakit Combobox](https://ariakit.org/components/combobox).
## When to use
Use `Combobox` for a list where the user types to filter — large static lists, async/server-side data, or any single-select where search is helpful. For small finite lists without filtering, use [`Select`](/components/select). For multi-selection, use [`Multi Select`](/components/multi-select).
Pair with `Field.*` for label/description/error wiring in forms — wrap `Combobox.Input` in `Field.Control`. `Field.Control` flows the generated `id`, `name`, `aria-describedby`, `aria-errormessage`, and `aria-invalid` onto the focusable input.
```tsx
import { Combobox } from "@ngrok/mantle/combobox";
import { Field } from "@ngrok/mantle/field";
SubdomainAvailable domainsStart typing to filter available domains.;
```
Or render the control on its own:
```tsx
import { Combobox } from "@ngrok/mantle/combobox";
import { CirclesThreePlusIcon } from "@phosphor-icons/react/CirclesThreePlus";
Choose an ngrok subdomain
Sit dolor enim eiusmod nulla nostrud officia in magna deserunt ut ex veniam cillum.
;
```
## Examples
### With TanStack Form
Use `@tanstack/react-form` with `zod` to keep the combobox input controlled. Wrap `Combobox.Input` in `Field.Control` so label, name, and validation ARIA are applied to the focusable input.
```tsx
import { Button } from "@ngrok/mantle/button";
import { Combobox } from "@ngrok/mantle/combobox";
import { Field, toErrorMessages } from "@ngrok/mantle/field";
import { useForm } from "@tanstack/react-form";
import { z } from "zod";
export const formSchema = z.object({
region: z.string().min(1, "Choose a region."),
});
function Example() {
const defaultValues = {
region: "",
};
const form = useForm({
defaultValues,
validators: {
onSubmit: formSchema,
},
onSubmit: ({ value }) => {
// Handle form submission here
},
});
return (
{(field) => (
Region {
field.handleChange(value);
}}
>
US EastUS WestEU West
)}
Submit
);
}
```
## Composition
Compose the parts of a `Combobox` together to build your own:
```text showLineNumbers=false
Combobox.Root
├── Combobox.Input
└── Combobox.Content
├── Combobox.Group
│ ├── Combobox.GroupLabel
│ └── Combobox.Item
│ └── Combobox.ItemValue
└── Combobox.Separator
```
## API Reference
The `Combobox` components are built on top of [ariakit Combobox](https://ariakit.org/components/combobox).
### Combobox.Root
Root component for a combobox. Provides a combobox store that controls the state of Combobox components.
All props from ariakit [ComboboxProvider](https://ariakit.org/reference/combobox-provider).
### Combobox.Input
Renders a combobox input element that can be used to filter a list of items.
All props from ariakit [Combobox](https://ariakit.org/reference/combobox), plus:
| Prop | Type | Default | Description |
| :-------------- | :------------------------------------------------------------------------------------------------- | :--------- | :---------------------------------------------------- |
| `autoComplete?` | `string` | `"list"` | The autocomplete behavior of the input. |
| `autoSelect?` | `"always" \| "inline" \| boolean` | `"always"` | How the first item is automatically selected. |
| `validation?` | `"error" \| "success" \| "warning" \| false \| (() => "error" \| "success" \| "warning" \| false)` | | Validation state that changes the border and outline. |
### Combobox.Content
Renders a popover that contains combobox content, e.g. items, groups, and separators.
All props from ariakit [ComboboxPopover](https://ariakit.org/reference/combobox-popover), plus:
| Prop | Type | Default | Description |
| :--------------- | :-------- | :------ | :-------------------------------------------------------------------- |
| `asChild?` | `boolean` | `false` | Compose onto an alternative element type or your own React component. |
| `sameWidth?` | `boolean` | `true` | Whether the popover should be the same width as the input. |
| `unmountOnHide?` | `boolean` | `true` | Whether the popover should unmount when hidden. |
### Combobox.Item
Renders a combobox item inside a `Combobox.Content` component.
All props from ariakit [ComboboxItem](https://ariakit.org/reference/combobox-item), plus:
| Prop | Type | Default | Description |
| :-------------- | :-------- | :------ | :-------------------------------------------------------------------- |
| `asChild?` | `boolean` | `false` | Compose onto an alternative element type or your own React component. |
| `focusOnHover?` | `boolean` | `true` | Whether the item should receive focus on hover. |
### Combobox.ItemValue
Highlights the match between the current `Combobox.Input` value and parent `Combobox.Item` value. Should only be used as a child of `Combobox.Item`.
All props from ariakit [ComboboxItemValue](https://ariakit.org/reference/combobox-item-value), plus:
| Prop | Type | Default | Description |
| :--------- | :-------- | :------ | :-------------------------------------------------------------------- |
| `asChild?` | `boolean` | `false` | Compose onto an alternative element type or your own React component. |
### Combobox.Group
Renders a group for `Combobox.Item` elements. Optionally, a `Combobox.GroupLabel` can be rendered as a child to provide a label for the group.
All props from ariakit [ComboboxGroup](https://ariakit.org/reference/combobox-group), plus:
| Prop | Type | Default | Description |
| :--------- | :-------- | :------ | :-------------------------------------------------------------------- |
| `asChild?` | `boolean` | `false` | Compose onto an alternative element type or your own React component. |
### Combobox.GroupLabel
Renders a label in a combobox group. Should be wrapped with `Combobox.Group` so the `aria-labelledby` is correctly set on the group element.
All props from ariakit [ComboboxGroupLabel](https://ariakit.org/reference/combobox-group-label), plus:
| Prop | Type | Default | Description |
| :--------- | :-------- | :------ | :-------------------------------------------------------------------- |
| `asChild?` | `boolean` | `false` | Compose onto an alternative element type or your own React component. |
### Combobox.Separator
Renders a separator between `Combobox.Item`s or `Combobox.Group`s.
All props from [Separator](/components/separator).
---
---
title: Command
description: A command palette that allows users to search and execute commands.
---
# Command
A command palette that allows users to search and execute commands. Built on top of [cmdk](https://cmdk.paco.me/).
### Command Example
```tsx
import { Command } from "@ngrok/mantle/command";
import { CalendarIcon, SmileyIcon, CalculatorIcon, UserIcon } from "@phosphor-icons/react";
No results found.CalendarSearch EmojiCalculatorProfile;
```
### Command Dialog Example
```tsx
import { Button } from "@ngrok/mantle/button";
import { Command, MetaKey } from "@ngrok/mantle/command";
import {
CalculatorIcon,
CalendarIcon,
CreditCardIcon,
GearIcon,
SmileyIcon,
UserIcon,
} from "@phosphor-icons/react";
import { useEffect, useState } from "react";
function useHotkey(key: string, callback: () => void) {
useEffect(() => {
const keydown = (event: KeyboardEvent) => {
if (event.key === key && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
callback();
}
};
document.addEventListener("keydown", keydown);
return () => document.removeEventListener("keydown", keydown);
});
}
function CommandDialogDemo() {
const [open, setOpen] = useState(false);
useHotkey("j", () => setOpen(!open));
return (
<>
Press{" "}
J
or
setOpen(!open)}
>
Open Command Dialog
No results found.CalendarSearch EmojiCalculatorProfile P
Billing B
Settings S
>
);
}
```
## Composition
Compose the parts of a `Command` together to build your own:
```text showLineNumbers=false
Command.DialogRoot
├── Command.DialogTrigger
└── Command.DialogContent
├── Command.Input
└── Command.List
├── Command.Empty
├── Command.Group
│ └── Command.Item
│ └── Command.Shortcut
└── Command.Separator
```
## API Reference
The `Command` component is built on top of [cmdk](https://cmdk.paco.me/) and provides a complete set of sub-components for building command palettes.
### Command.Root
The root component for the Command. It provides the context for all other command sub-components.
All props from cmdk's [Command Root](https://github.com/pacocoursey/cmdk?tab=readme-ov-file#command-cmdk-root).
### Command.DialogRoot
The root stateful component for the Command dialog. Manages open/closed state.
All props from Radix UI's [Dialog.Root](https://www.radix-ui.com/primitives/docs/components/dialog#root).
### Command.DialogTrigger
A button that opens the Command dialog when clicked. Supports `asChild` for custom trigger elements.
All props from Radix UI's [Dialog.Trigger](https://www.radix-ui.com/primitives/docs/components/dialog#trigger).
### Command.DialogContent
The visible content of the Command dialog. Renders inside the dialog portal and wraps the command palette UI.
| Prop | Type | Default | Description |
| :----------------- | :------------------------------------------ | :--------------------------------- | :------------------------------------------------------- |
| `title?` | `string` | `"Command Palette"` | Accessible title for the dialog (visually hidden). |
| `description?` | `string` | `"Search for a command to run..."` | Accessible description for the dialog (visually hidden). |
| `className?` | `string` | | Class name(s) to apply to the dialog content. |
| `showCloseButton?` | `boolean` | `true` | Whether to show the close button. |
| `filter?` | `(value: string, search: string) => number` | | Custom filter function for the command list. |
| `shouldFilter?` | `boolean` | | Whether to enable built-in filtering of command items. |
### Command.Input
The input component for the Command. It provides the input for the command palette.
All props from cmdk's [Command Input](https://github.com/pacocoursey/cmdk?tab=readme-ov-file#input-cmdk-input).
### Command.List
The list component for the Command. It provides the scrollable list for the command palette.
All props from cmdk's [Command List](https://github.com/pacocoursey/cmdk?tab=readme-ov-file#list-cmdk-list).
### Command.Empty
The empty component for the Command. Displayed when no results match the search query.
All props from cmdk's [Command Empty](https://github.com/pacocoursey/cmdk?tab=readme-ov-file#empty-cmdk-empty).
### Command.Group
The group component for the Command. Used to group related command items together.
All props from cmdk's [Command Group](https://github.com/pacocoursey/cmdk?tab=readme-ov-file#group-cmdk-group-hidden).
### Command.Item
The item component for the Command. Represents a selectable command in the palette.
All props from cmdk's [Command Item](https://github.com/pacocoursey/cmdk?tab=readme-ov-file#item-cmdk-item-data-disabled-data-selected).
### Command.Separator
A visual separator between command groups or items. Automatically hidden when there is an active search query.
All props from cmdk's [Command Separator](https://github.com/pacocoursey/cmdk?tab=readme-ov-file#separator-cmdk-separator).
### Command.Shortcut
Displays a keyboard shortcut hint within a command item. Renders as a styled `span` element.
All props from the HTML `span` element.
### MetaKey
Renders the platform-appropriate meta key label (`⌘` for macOS/iOS or `Ctrl` for other platforms). It detects the platform on mount and is SSR-safe, defaulting to `Ctrl` to avoid hydration mismatches.
Use it in keyboard shortcut hints and `Command.Shortcut` labels to ensure the correct modifier key is displayed for each platform.
```tsx
import { Command, MetaKey } from "@ngrok/mantle/command";
K
S
```
### useCommandState
A hook for accessing the command palette state.
All props from cmdk's [useCommandState](https://github.com/pacocoursey/cmdk?tab=readme-ov-file#usecommandstatestate--stateselectedfield).
---
---
title: Data Table
description: Tables purposefully designed for dynamic, application data with features like sorting, filtering, and pagination.
---
# Data Table
Tables purposefully designed for dynamic, application data with features like sorting, filtering, and pagination. Powered by [TanStack Table](https://tanstack.com/table/latest/docs/introduction).
## When to use
A `DataTable` is for **dynamic, application data** — anywhere users need to sort, filter, paginate, select, or click rows. It is built on top of [`Table`](/components/table) and wires up [TanStack Table](https://tanstack.com/table/latest/docs/introduction) so you get those behaviors out of the box.
- Prefer [`Table`](/components/table) for **static, presentational** tabular content (pricing matrices, reference tables, invoice summaries).
- All TanStack Table utilities (`createColumnHelper`, `getCoreRowModel`, `getSortedRowModel`, `getPaginationRowModel`, `getFilteredRowModel`, `useReactTable`, etc.) are re-exported from `@ngrok/mantle/data-table` — you do not need to add `@tanstack/react-table` as a separate dependency.
## Quick start
The minimum viable `DataTable`. Copy, paste, replace the type and data:
```tsx
import {
DataTable,
createColumnHelper,
getCoreRowModel,
useReactTable,
} from "@ngrok/mantle/data-table";
type Row = { id: string; name: string };
const columnHelper = createColumnHelper();
const columns = [
columnHelper.accessor("name", {
id: "name",
header: (props) => (
Name
),
cell: (props) => {props.getValue()},
}),
];
function MyTable({ data }: { data: Row[] }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
const rows = table.getRowModel().rows;
return (
{rows.length > 0 ? (
rows.map((row) => )
) : (
No results.
)}
);
}
```
A fuller example matching the demo above — sortable columns, pagination, filtering, row-click navigation, and a sticky action column:
```tsx
import {
DataTable,
createColumnHelper,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@ngrok/mantle/data-table";
import { Empty } from "@ngrok/mantle/empty";
import { TrayIcon } from "@phosphor-icons/react/Tray";
import { href, useNavigate } from "react-router";
import { useMemo } from "react";
type Payment = {
id: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
};
const columnHelper = createColumnHelper();
const columns = [
columnHelper.accessor("id", {
id: "id",
header: (props) => (
ID
),
cell: (props) => {props.getValue()},
}),
// ... more columns
];
function PaymentsExample() {
const navigate = useNavigate();
const data = useMemo(() => examplePayments, []);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
initialState: {
sorting: [{ id: "email", desc: false }],
pagination: { pageSize: 100 },
},
});
const rows = table.getRowModel().rows;
return (
{rows.length > 0 ? (
rows.map((row) => (
{
navigate(href("/payments/:id", { id: row.original.id }));
}}
row={row}
/>
))
) : (
} />
No payments yet
Payments you receive will appear here.
)}
);
}
```
## Composition
Compose the parts of a `DataTable` together to build your own:
```text showLineNumbers=false
DataTable.Root
├── DataTable.Head
│ └── DataTable.Row
│ ├── DataTable.Header
│ │ └── DataTable.HeaderSortButton
│ └── DataTable.ActionHeader
└── DataTable.Body
├── DataTable.Row
│ ├── DataTable.Cell
│ └── DataTable.ActionCell
└── DataTable.EmptyRow
```
## Rules
Follow these invariants for a correctly styled, accessible, and behaving `DataTable`.
1. **Type columns with `createColumnHelper()`.** It threads `TData` through `header`, `cell`, and `row.original` so consumers get inference instead of `unknown`.
2. **Wrap every body cell in `DataTable.Cell`.** A raw `
` skips the mantle typography, padding, and sticky-column styling.
3. **Wrap every header in `DataTable.Header`.** For sortable columns, also wrap its contents in `DataTable.HeaderSortButton` — the icon, cycling behavior, and screen-reader announcements are provided by that button.
4. **Key rows with `row.id`.** TanStack Table tracks row identity across sort/filter/pagination; using array indexes will re-mount rows incorrectly.
5. **Always branch on `rows.length > 0` and render `DataTable.EmptyRow`** for the empty case. The empty row spans all columns and preserves the table's frame — returning `null` leaves an empty `
` and collapses the frame.
6. **Place an action column last, using `columnHelper.display({ ... })`.** Pair `DataTable.ActionHeader` (in `header`) with `DataTable.ActionCell` (in `cell`) so the pinned column aligns across header and body when scrolling horizontally.
7. **Pass `onClick` to `DataTable.Row` for row-click behavior.** The row auto-applies `cursor-pointer` when `onClick` is set — do not add it yourself. Override with another `cursor-*` class (for example, `cursor-wait`) via `className` if needed.
8. **Stop click propagation inside `DataTable.ActionCell` when the row is clickable.** Without it, clicks on dropdown triggers, buttons, and links inside the action cell will bubble and fire the row `onClick`.
9. **Provide a keyboard-accessible equivalent for row navigation.** A `
` is not focusable and is not announced as interactive to assistive tech. If clicking a row navigates, render a `` in the primary cell so keyboard and screen-reader users have a reachable equivalent.
10. **Register the row models you use.** `useReactTable` only wires up what you pass in: `getSortedRowModel()` for sorting, `getPaginationRowModel()` for pagination, `getFilteredRowModel()` for filtering. Missing one and the corresponding feature silently no-ops.
## Anti-patterns
Common mistakes. The left column is what not to do; the right column is the fix.
```tsx
// ❌ Raw
— misses mantle styling
cell: (props) =>
{props.getValue()}
,
// ✅
cell: (props) => {props.getValue()},
// ❌ Manual cursor-pointer — redundant, can desync from behavior
// ✅
// ❌ Plain button for a sortable header — no icon, no ARIA
header: () => column.toggleSorting()}>Name,
// ✅
header: (props) => (
Name
),
// ❌ Clickable row with a dropdown inside — trigger click fires the row onClick
...
// ✅ Stop propagation at the action cell boundary
event.stopPropagation()}>
...
// ❌ Empty state returns null — collapses the table frame
{rows.map((row) => )}
// ✅ Use DataTable.EmptyRow
{rows.length > 0
? rows.map((row) => )
: No results.}
// ❌ Declared sorting but forgot the row model
useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });
// ✅
useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
```
## Recipes
### Empty states (no data vs. no results)
A list has **two** distinct empty states, and they need different copy and actions:
1. **No data** — nothing exists yet. Informational, optionally a primary "create" action.
2. **No results for the active filter/search** — the user filtered to nothing and needs a way out: a **Clear filters** button.
Branch on `rows.length`, then on whether a filter is active, and host an [`Empty`](/components/empty) inside `DataTable.EmptyRow` for each branch. `EmptyRow` already spans every column (auto `colSpan`) and `Empty.Root` centers itself — drop a single `Empty.Root` in as the child; don't hand-roll a `
` or any centering. Type into the filter below to see the "no results" branch and its working `Clear filters` reset:
```tsx
import { Button } from "@ngrok/mantle/button";
import { DataTable } from "@ngrok/mantle/data-table";
import { Empty } from "@ngrok/mantle/empty";
import { MagnifyingGlassIcon } from "@phosphor-icons/react/MagnifyingGlass";
import { TrayIcon } from "@phosphor-icons/react/Tray";
const rows = table.getRowModel().rows;
const isFiltered = globalFilter.trim() !== ""; // or table.getState().columnFilters.length > 0
{rows.length > 0 ? (
rows.map((row) => )
) : isFiltered ? (
} />
No results match your filter
Try a different search, or clear the filter to see everything.