Collapsible JSON tree viewer
A recursive React component that renders any JSON value as an expandable tree with typed colours and item counts.
React / Next.jsUtilitiesreactjsontreerecursion
Live preview
Code
'use client';
import { useState } from 'react';
export type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
const scalarColour = (v: Json) =>
typeof v === 'string' ? 'text-emerald-600'
: typeof v === 'number' ? 'text-sky-600'
: typeof v === 'boolean' ? 'text-violet-600'
: 'text-slate-400';
function Node({ name, value, depth, openDepth }: { name?: string; value: Json; depth: number; openDepth: number }) {
const isBranch = value !== null && typeof value === 'object';
const [open, setOpen] = useState(depth < openDepth);
const label = name !== undefined && <span className="text-slate-700">{name}: </span>;
if (!isBranch) {
return (
<li className="font-mono text-sm leading-6">
{label}
<span className={scalarColour(value)}>{JSON.stringify(value)}</span>
</li>
);
}
const entries = Object.entries(value as object) as [string, Json][];
const isArray = Array.isArray(value);
const [openBr, closeBr] = isArray ? ['[', ']'] : ['{', '}'];
return (
<li className="font-mono text-sm leading-6">
<button
type="button"
aria-expanded={open}
onClick={() => setOpen((o) => !o)}
className="text-left hover:text-slate-950"
>
<span className="mr-1 inline-block w-3 text-slate-400">{open ? '▾' : '▸'}</span>
{label}
<span className="text-slate-500">
{openBr}
{open ? '' : `${entries.length}${closeBr}`}
</span>
</button>
{open && (
<>
<ul className="ml-4 border-l border-slate-200 pl-3">
{entries.map(([k, v]) => (
<Node key={k} name={k} value={v} depth={depth + 1} openDepth={openDepth} />
))}
</ul>
<span className="ml-4 text-slate-500">{closeBr}</span>
</>
)}
</li>
);
}
export function JsonTree({ data, openDepth = 1 }: { data: Json; openDepth?: number }) {
return (
<ul className="rounded-lg bg-slate-50 p-3">
<Node value={data} depth={0} openDepth={openDepth} />
</ul>
);
}
How it works
What it solves
Pretty-printed JSON is fine for small payloads, but a large API response becomes unreadable without folding. This component lets people collapse branches, see how many children each one holds and scan values by colour, without pulling in a tree-view library.
How it works
Node is a recursive component. For objects and arrays it renders a button that toggles local open state, followed by the children in an indented list; for scalars it renders a single line coloured by type. Initial openness comes from a depth budget: nodes shallower than openDepth start expanded, which gives a sensible first view of a deep document.
Entries are produced with Object.entries for both arrays and objects, so array indexes appear as keys. A collapsed branch shows a summary such as {3} or [12] so the shape is visible even when folded. The toggle uses aria-expanded so keyboard and screen-reader users get the same affordance.
Adapting it
- Raise
openDepthto expand more by default, or pass a very large number for a fully open tree. - Add a copy-path feature by threading a
pathstring through the recursion. - Truncate long strings with CSS
truncateand atitleattribute.
Pitfalls
- Each node owns its state, so changing
datakeeps old open flags only if keys line up; give the root akeyto reset. - Extremely large arrays render every child when open. Virtualise or paginate beyond a few thousand rows.
- JSON from
JSON.parseis acyclic, so recursion is safe; do not feed it arbitrary object graphs.
How to use it
- Save as
components/JsonTree.tsx(client component, Next.js 15 / React 19). - No dependencies; the Tailwind classes can be replaced freely.
- Parse your text first:
<JsonTree data={JSON.parse(text)} />. - Use
openDepth={2}to start two levels deep; the default is one.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.