DEV3 Technologies home

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 openDepth to expand more by default, or pass a very large number for a fully open tree.
  • Add a copy-path feature by threading a path string through the recursion.
  • Truncate long strings with CSS truncate and a title attribute.

Pitfalls

  • Each node owns its state, so changing data keeps old open flags only if keys line up; give the root a key to reset.
  • Extremely large arrays render every child when open. Virtualise or paginate beyond a few thousand rows.
  • JSON from JSON.parse is acyclic, so recursion is safe; do not feed it arbitrary object graphs.

How to use it

  1. Save as components/JsonTree.tsx (client component, Next.js 15 / React 19).
  2. No dependencies; the Tailwind classes can be replaced freely.
  3. Parse your text first: <JsonTree data={JSON.parse(text)} />.
  4. 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.

Related snippets