@gfazioli/mantine-json-tree
A Mantine extension component that renders interactive JSON trees with syntax highlighting, collapsible nodes, in-place editing of values and structure, copy-to-clipboard, and configurable expansion depth.
Upgrading from an older major version? See the Upgrade guide for breaking changes and step-by-step migration instructions.
What's new
- Structural editing — rename keys, add and remove keys and items, reorder arrays (
structuralEdits) - Entry limit — long objects and arrays end with a "… N more" row that reveals the next page (
maxDisplayLength) - Locked expansion — every node open, none can be collapsed (
allExpanded) - Quote style — show or hide the quotes around strings and keys (
withQuotes,withKeyQuotes) - Labels — translate every built-in string, and every icon button now has an accessible name (
labels) - Indentation width (
indentWidth) and a root without a label (rootName={false}) - Diff highlighting — mark nodes as added, removed or changed (
highlightNode) - Large arrays split into collapsible
[start…end]groups (groupArraysAfterLength) - Long strings cut with a "show more" toggle (
collapseStringsAfterLength) - Type badges next to every value (
showValueTypes) - Sorted keys, alphabetical or with your own comparator (
sortKeys) - Editable values — click a string, number or boolean to edit it in place (
editable,onChange) - Circular references are detected and marked instead of crashing the component
- Search with text highlight, auto-expand, and filtered tree view (
withSearch) - Redesigned toolbar with key count badge, global copy, search toggle, and modern icons
- Paper wrapper with
withBorderfor bordered container look - Root name customization (
rootNameprop) - Global copy button to copy entire JSON to clipboard (
withCopyAll) - Key count badge showing total keys/items next to title (
withKeyCountBadge) - Dark mode support with automatic color adaptation
- Line numbers display (
showLineNumbers) - Path tooltip on hover (
showPathOnHover) - Max height with scrollable container (
maxHeight) - Controlled expand/collapse state (
expanded,onExpandedChange) onExpandandonCollapsecallbacks for individual node toggling- Keyboard copy:
Ctrl+C/Cmd+Ccopies the focused node whenwithCopyToClipboardis enabled - Responsive
sizeprop via Mantine breakpoint objects (CSS-native, no re-renders)
Installation
After installation import package styles at the root of your application:
You can import styles within a layer @layer mantine-json-tree by importing @gfazioli/mantine-json-tree/styles.layer.css file.
Comparison with Mantine JsonViewer
Since version 9.7, Mantine ships a first-party JsonViewer in @mantine/code-highlight. The basics overlap — collapsible nodes, copy to clipboard, type badges, diff highlighting, keyboard navigation — but the two make different trade-offs.
| Capability | JsonTree | JsonViewer |
|---|---|---|
In-place editing Strings, numbers and booleans, with validation and an immutable onChange | ||
Structural editing Rename keys, add and remove keys and items, reorder an array — expanded nodes and focus follow every change | ||
Search Filter keys and values, with highlighted matches and the matching branches expanded | ||
JavaScript values as they are Date, RegExp, Map, Set, BigInt, Symbol, NaN, Infinity, functions and React elements — core shows what JSON.stringify would: a Date as its ISO string, a Map or a Set as an empty object | ||
Header Title, key count badge and a sticky header with its toolbar | ||
Indent guides Colored vertical lines for each nesting level | ||
Path tooltip The full path of a node on hover | ||
Responsive font size size accepts breakpoint maps, applied with CSS only | ||
Function display displayFunctions shows a function as a label, hides it or expands its properties | ||
Expand all, collapse all, copy all withExpandAll and withCopyAll here, withControls and withCopyButton in core | ||
Copy a node Both: a copy button on each row and Ctrl / ⌘ + C on the focused node | ||
Keyboard navigation Both: an ARIA tree driven by the arrow keys — JsonTree through Mantine's Tree | ||
Type badges and item counts showValueTypes / showItemsCount here, withTypes / withSize in core | ||
Sorted keys, long strings, long arrays sortKeys, collapseStringsAfterLength and groupArraysAfterLength in both | ||
Entry limit maxDisplayLength in both — here the "… N more items" row is a button that reveals the next page, and a search still reaches every entry; in core it is plain text | ||
Locked expansion allExpanded in both: every node open, array groups included, and collapsing disabled | ||
Quote style withQuotes and withKeyQuotes in both, with the same defaults | ||
Indentation width indentWidth in both: 32px per level by default here, 16px in core | ||
Root label rootName names the root, or false hides it. The root is named 'root' by default here, unnamed in core | ||
Translatable labels labels replaces the built-in strings — buttons, toggles, type badges, editor names and messages — and names every icon button for screen readers; core takes copy, copied, expand all and collapse all | ||
Diff highlighting JsonTree: a highlightNode callback with added, removed and changed — core: a highlightItems map with added and removed | ||
Circular references Both mark a value that points back at an ancestor as [Circular] | ||
Line numbers and controlled expansion Both: line numbers, and the expanded nodes as controlled state |
When to use which. Reach for JsonViewer for a light, read-only view of JSON that came over the wire, especially if you already use @mantine/code-highlight. Reach for JsonTree when the data is a live JavaScript value — Dates, Maps, Sets, functions, circular graphs — when you need to search it, or to edit it in place, values and structure alike, or when the tree is a tool of its own, with a title, a sticky toolbar and indent guides.
Usage
The JsonTree is interactive JSON tree viewer component built with Mantine's Tree component. Features collapsible nodes, syntax highlighting with type-specific colors, copy-to-clipboard functionality, item count badges, configurable expansion depth, and smooth animations. Perfect for debugging API responses, exploring complex data structures, and developer tools.
- root:{
- name:
"John Doe" - age:
30 - isAdmin:
false - courses:[...]3
- wife:
null - onClick:
[Function: onClick] - address:{...}3
- action:{...}2
- projects:[...]2
Editing Values
Set editable to let a reader change a value in place. Click a string or a number to open an
inline editor — Enter commits, Escape cancels, moving focus commits — or click a boolean to
toggle it outright, since it has exactly one other state. With a row focused, Enter
opens its editor too, so the whole flow works from the keyboard without adding a tab stop per value.
JsonTree is controlled while editing: it never keeps its own copy of your data. onChange
hands you the next value, and it only takes effect once you feed it back through data.
data is typed unknown, so onChange hands the next value back as unknown too. Editing a
value never changes the shape of the object — a string stays a string, a number a number — so
casting it back to your own type is safe:
The original object is never mutated. Only the spine down to the edited node is rebuilt, so every
Date, Map, Set, RegExp, BigInt, function and React element elsewhere in the tree keeps
its identity — a structuredClone or a JSON round-trip would flatten or destroy all of them.
- root:{
- name:
"Jamie Chen" - age:
34 - isAdmin:
false - createdAt:
2024-01-15T10:30:00.000Z - address:{
- city:
"Anytown" - zip:
"12345"
- tags:[
- 0:
"react" - 1:
"mantine"
Click a value to edit it. Enter commits, Escape cancels.
What can be edited
editableTypes defaults to ['string', 'number', 'boolean']; anything outside it stays read-only.
Two kinds of node can never be edited, whatever you put in that list:
MapandSetentries. They are displayed under a synthetic key — aMapkey can be any value at all, including an object — so there is no address to write back to.- Function properties expanded with
displayFunctions="as-object". Those properties belong to a synthetic object that does not exist in your data.
isEditable gates individual nodes, and validate rejects a value by returning a message:
Addressing a node
onChange receives a change object carrying both ways of naming the node:
path— the display label, e.g.root.address.city. It is not unique:{ 'a.b': 1 }and{ a: { b: 1 } }both produceroot.a.b, and an array index reads the same as a numeric object key.pathSegments— the real address, one step per level, with object keys as strings and array indices as numbers. This is what the write is performed against, so an edit always lands on the node you clicked.
Use path for display and logging; use pathSegments whenever you need to act on the node.
Editing Structure
Add structuralEdits to editable and the reader can change the shape of the data too: rename
an object key, add a key or an item, remove one, and move the items of an array. Pass true for
all four, or a list such as ['add', 'remove'] for some of them.
- Rename: click a key, or press
F2on its row, and type the new name. A key the object already has is refused, andvalidateKeycan refuse others. - Add: the
+button on an object asks for the new key; on an array it appends an item. A new entry starts with the type of the container's last entry, emptied ('',0,false,null,{},[]), or with whatevergetNewValuereturns; a new string or number opens its editor at once. - Remove: the bin button, or
Delete/Backspaceon the focused row. - Reorder: the arrow buttons on an array item, or
Alt + ↑ / ↓.
- root:{
- name:
"release-checklist" - owner:
"Jamie Chen" - steps:[
- 0:
"bump version" - 1:
"build" - 2:
"publish" - 3:
"announce"
- settings:{
- dryRun:
false - retries:
3
Hover a row: + adds, the arrows move an item, the bin removes. Click a key to rename it.
onChange reports each change with an action — 'edit' for a value, or 'rename', 'add',
'remove' and 'reorder' — and a rename or a reorder also carries previousPath,
previousPathSegments and previousKey. Every write goes through pathSegments, so it lands on
the node you meant even when two keys share a display path, and, as with values, only the spine
down to the change is rebuilt.
Keys and indices are part of every path below them, so a rename or a shifted index changes the
path of a whole subtree. JsonTree carries its own state across: expanded nodes stay expanded
under their new paths (a controlled expanded gets the new list through onExpandedChange),
revealed pages stay revealed, and focus follows the change — to the renamed row, the moved item,
or the entry that took a removed one's place. The same entries as for values stay read-only:
Map and Set entries, the [start…end] groups, reference cycles, and whatever isEditable
refuses.
Search
Enable withSearch to add a search toggle in the toolbar.
When active, the tree filters to show only branches with
matching keys or values. Direct matches get an amber
background highlight, and the matching text portion is
highlighted inline. Parent nodes are preserved for context.
Clearing the search restores the previous expand state.
- root:{
- id:
"usr_7k2m9p" - email:
"jamie@example.com" - name:
"Jamie Chen" - role:
"admin" - verified:
true - created_at:
"2026-03-10T23:42:00Z" - metadata:{
- login_count:
142 - last_ip:
"203.0.113.42" - preferences:{...}
Customizing the search input
Use searchInputProps to forward any prop to the internal
TextInput — classNames, styles, vars, variant,
radius, size, leftSection, and so on. Because Mantine
applies styles inline, this works without specificity hacks
or !important.
- root:{
- user:{
- id:
"usr_42" - name:
"Jamie" - role:
"admin"
- metadata:{
- theme:
"dark" - notifications:
true
Copy to Clipboard
Both per-node and global copy buttons provide visual feedback — the icon briefly changes to a green checkmark after a successful copy.
The global copy button (withCopyAll) copies the raw JSON data without the rootName wrapper.
The rootName is a display-only label and is not included in the copied output. This makes the
copied JSON directly usable in code and API tools.
What lands on the clipboard is what the tree shows. JSON.stringify alone cannot express
everything the tree renders — it returns undefined for functions, symbols and undefined, and
throws on BigInt and on any value holding a reference cycle — so those fall back to the rendered
form rather than copying the literal text undefined or silently doing nothing.
Root Name
Use the rootName prop to customize the label of the
root node. Defaults to "root". This is useful for API
responses, config files, or when displaying multiple
trees with different root labels. Note that rootName
is display-only — it does not affect the copied JSON
output. Pass rootName={false} to show the root without a label;
paths keep their root prefix, so expanded, onNodeClick and
the path tooltip are the same either way:
- response:{
- id:
1 - name:
"Alice" - role:
"admin"
- items:[
- 0:
1 - 1:
2 - 2:
3
- {
- id:
1 - name:
"Alice" - role:
"admin"
Syntax Highlighting values
Below is an example of syntax highlighting for different JSON value types: strings, numbers, booleans, nulls, objects, and arrays. Each type is displayed in a distinct color for better readability.
- root:
"Hello, World!"
- root:
42
- root:
true
- root:
false
- root:
null
- root:{
- key1:
"value1" - key2:
123 - key3:
false - key4:
null
- root:[
- 0:
"string" - 1:
456 - 2:
true - 3:
null - 4:{
- nestedKey:
"nestedValue"
Quote Style
Strings are shown between quotes and object keys without them, as Mantine's JsonViewer does by
default. withQuotes={false} drops the quotes around strings, and withKeyQuotes adds them
around object keys — never around an array index, a Map or Set entry or a group label, which
JSON would not quote either. Search matches the text as it is displayed, and copying still
produces valid JSON.
- root:{
- "name":
"Ada" - "languages":[
- 0:
"en" - 1:
"fr"
- "address":{
- "city":
"London"
- root:{
- name:
Ada - languages:[
- 0:
en - 1:
fr
- address:{
- city:
London
Special Value Types
JsonTree supports modern JavaScript types beyond standard JSON primitives. These include Date objects, special numeric values (NaN, Infinity), BigInt for large integers, Symbols, RegExp patterns, and ES6 collections like Map and Set. Each type is displayed with distinct syntax highlighting and formatting:
- Date: Displayed in ISO 8601 format (e.g.,
2024-01-15T10:30:00.000Z) - NaN / Infinity: Special numeric values shown with their standard JavaScript representation
- BigInt: Large integers displayed with the
nsuffix (e.g.,9007199254740991n) - Symbol: Unique identifiers shown with their description (e.g.,
Symbol(unique)) - RegExp: Regular expressions displayed with their pattern and flags (e.g.,
/pattern/gi) - Map: Key-value collections that are expandable to show their entries
- Set: Unique value collections that are expandable to show their elements
- React Elements: React components displayed with their component name (e.g.,
<Loader />)
React elements are recognised by their internal $$typeof marker, so ordinary data is never
mistaken for one. An object such as { type: 'text', props: { label: 'Name' } } — a common
shape in form-builder and low-code JSON — is rendered as a regular, expandable object.
All colors can be customized using CSS variables for each type.
- root:{
- reactLoader:
<@mantine/core/Loader /> - reactButton:
<button /> - htmlDiv:
<div /> - createdAt:
2024-01-15T10:30:00.000Z - lastModified:
2024-06-20T14:45:30.000Z - notANumber:
NaN - positiveInfinity:
Infinity - negativeInfinity:
-Infinity - bigInteger:
9007199254740991n - userId:
123456789n - globalSymbol:
Symbol(app.config) - registryKey:
Symbol(app.registry) - emailPattern:
/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/g - phonePattern:
/\d{3}-\d{3}-\d{4}/ - userMap:{
- [0] user1:{...}2
- [1] user2:{...}2
- [2] 123:
"numeric key example"
- tags:{
- 0:
"javascript" - 1:
"typescript" - 2:
"react"
- uniqueNumbers:{
- 0:
1 - 1:
2 - 2:
3 - 3:
4 - 4:
5
- metadata:{
- timestamp:
2024-12-25T00:00:00.000Z - categories:{...}2
- config:{...}2
Circular References
Object graphs that point back at themselves are everywhere in real debugging sessions — a child
holding a reference to its parent, a linked list, a cached entity pointing at the store that owns
it. JsonTree walks the current branch and stops as soon as a value reappears on its own ancestor
chain, rendering it as [Circular] rather than recursing until the stack overflows.
Only the ancestor chain is tracked, never every value already visited. A value referenced from two
different branches is shared, not circular, so it keeps expanding normally in both places — as the
config object does below.
- root:{
- id:
"root" - config:{
- theme:
"dark" - locale:
"en"
- child:{
- id:
"child" - config:{
- theme:
"dark" - locale:
"en"
- parent:
[Circular]
- self:
[Circular]
Diff Highlighting
highlightNode is called for every node except the synthetic [start…end] groups of groupArraysAfterLength, and returns how it changed: 'added', 'removed', 'changed', or null to leave it as is. It receives the node's path, pathSegments, key, type and value, so a diff can be computed against any previous version of the data — below, by reading the same pathSegments in both versions.
The colors come from the --json-tree-highlight-added-color, --json-tree-highlight-removed-color and --json-tree-highlight-changed-color variables, and each highlighted row carries a data-json-tree-highlight attribute for your own styles.
- root:{
- name:
"Alice" - age:
31 - role:
"editor" - tags:[
- 0:
"admin" - 1:
"billing"
- address:{
- city:
"Milan" - zip:
"00100"
- email:
"alice@example.com"
Large Data
Three props keep big payloads readable:
groupArraysAfterLengthsplits an array longer than the given length into collapsible[start…end]groups of that many items, so a large array does not render every item at once. Each group is one more level of the tree (and ofmaxDepth); the items inside keep their own paths, so search, copy and editing work as before.collapseStringsAfterLengthcuts longer strings and adds a "show more" toggle. While a search is active the whole string is shown, so a match past the cut stays visible.showValueTypesadds a type badge next to every value:string,int,float,bool,null,object,array,dateand so on.
- root:{object
- description:
"A long description that would push every…"string - scores:[...]250array
- owner:{...}3object
Entry limit
maxDisplayLength shows at most that many entries of an object, array, Map or Set, and closes
it with a "… N more items" row. That row is a button: a click, or Enter on the focused row,
reveals the next as many entries and moves focus to the first of them. An array split by
groupArraysAfterLength is left whole, and the limit is off while a search is active, so no match
hides behind it. Copy, search, expand all and the key count badge always work on the full value.
- root:{
- users:[
- 0:{...}
- 1:{...}
- 2:{...}
- 3:{...}
- 4:{...}
- 5:{...}
- 6:{...}
- 7:{...}
- 8:{...}
- 9:{...}