Bonus
JSON tree viewer
Collapsible nodes with types and counts
Collapsible nodes with types and counts
Create a JSON viewer with Tailwind CSS. Use a max-w-2xl card with a toolbar holding a title, a muted node count, and two small buttons for expand all and collapse all, plus a copy button. The body renders a nested structure as indented rows: object and array nodes are buttons with a rotating chevron, the key in a medium weight, a muted type label, and a count of children in brackets; leaf rows show the key, a colon, and the value coloured by type — strings in green, numbers in blue, booleans in purple and null in muted grey — with strings quoted. Indentation is done with left padding and a faint vertical guide line per level. Then add JavaScript that renders the tree from an object literal, toggles a node open or closed, implements expand and collapse all, and copies the formatted JSON to the clipboard. <section class="mx-auto max-w-2xl px-6 py-14">
<div class="overflow-hidden rounded-2xl border border-neutral-200 bg-white">
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-neutral-200 px-5 py-3">
<div>
<p class="text-sm font-semibold text-neutral-900">response.json</p>
<p id="jsonCount" class="text-xs text-neutral-400"></p>
</div>
<div class="flex gap-2">
<button id="jsonExpand" class="rounded-lg border border-neutral-200 px-2.5 py-1.5 text-xs font-medium text-neutral-600 transition hover:bg-neutral-50">Expand all</button>
<button id="jsonCollapse" class="rounded-lg border border-neutral-200 px-2.5 py-1.5 text-xs font-medium text-neutral-600 transition hover:bg-neutral-50">Collapse all</button>
<button id="jsonCopy" class="rounded-lg bg-neutral-900 px-2.5 py-1.5 text-xs font-medium text-white transition hover:bg-neutral-800">Copy</button>
</div>
</div>
<div id="jsonTree" class="overflow-x-auto p-4 font-mono text-[13px] leading-relaxed"></div>
</div>
</section> var DATA = {
id: "rep_8f21c94a",
name: "Monthly revenue",
published: true,
version: 4,
owner: { id: "usr_4471", name: "Mira Osei", email: "[email protected]", admin: true },
schedule: { cron: "0 9 1 * *", timezone: "Europe/Amsterdam", recipients: ["[email protected]", "[email protected]"] },
filters: [
{ field: "region", op: "in", value: ["EU", "UK"] },
{ field: "plan", op: "eq", value: "scale" },
],
lastRun: null,
};
var tree = document.getElementById("jsonTree");
var countEl = document.getElementById("jsonCount");
var nodes = 0;
function valueHtml(value) {
if (value === null) return '<span class="text-neutral-400">null</span>';
if (typeof value === "string") return '<span class="text-green-600">"' + value + '"</span>';
if (typeof value === "number") return '<span class="text-blue-600">' + value + "</span>";
if (typeof value === "boolean") return '<span class="text-purple-600">' + value + "</span>";
return "";
}
function render(value, key, depth) {
nodes += 1;
var pad = depth * 16;
if (value !== null && typeof value === "object") {
var isArray = Array.isArray(value);
var keys = isArray ? value.map(function (_, i) { return i; }) : Object.keys(value);
var wrap = document.createElement("div");
wrap.style.paddingLeft = pad + "px";
if (depth > 0) wrap.className = "border-l border-neutral-100";
var head = document.createElement("button");
head.className = "json-node flex items-center gap-2 py-0.5 text-left";
head.innerHTML =
'<span class="chev text-neutral-400 transition-transform">▾</span>' +
(key === null ? "" : '<span class="font-medium text-neutral-900">' + key + "</span><span class=\"text-neutral-400\">:</span>") +
'<span class="text-neutral-400">' + (isArray ? "array" : "object") + "</span>" +
'<span class="text-neutral-300">[' + keys.length + "]</span>";
var children = document.createElement("div");
keys.forEach(function (k) {
children.appendChild(render(value[k], String(k), depth + 1));
});
head.addEventListener("click", function () {
var hidden = children.classList.toggle("hidden");
head.querySelector(".chev").classList.toggle("-rotate-90", hidden);
});
wrap.appendChild(head);
wrap.appendChild(children);
return wrap;
}
var leaf = document.createElement("div");
leaf.style.paddingLeft = pad + "px";
leaf.className = "py-0.5" + (depth > 0 ? " border-l border-neutral-100" : "");
leaf.innerHTML =
'<span class="pl-5"><span class="font-medium text-neutral-900">' + key + '</span><span class="text-neutral-400">: </span>' + valueHtml(value) + "</span>";
return leaf;
}
tree.appendChild(render(DATA, null, 0));
countEl.textContent = nodes + " nodes";
function setAll(open) {
Array.prototype.slice.call(tree.querySelectorAll(".json-node")).forEach(function (head) {
var children = head.nextElementSibling;
if (!children) return;
children.classList.toggle("hidden", !open);
head.querySelector(".chev").classList.toggle("-rotate-90", !open);
});
}
document.getElementById("jsonExpand").addEventListener("click", function () {
setAll(true);
});
document.getElementById("jsonCollapse").addEventListener("click", function () {
setAll(false);
});
document.getElementById("jsonCopy").addEventListener("click", function () {
var button = document.getElementById("jsonCopy");
navigator.clipboard.writeText(JSON.stringify(DATA, null, 2)).then(function () {
button.textContent = "Copied";
setTimeout(function () {
button.textContent = "Copy";
}, 1500);
});
});