Mischief

35 / Documents

JSON Viewer

A collapsible tree for a JSON payload, navigable from the keyboard, where every row can hand you its path.

response{ 7 items }
id:"run_8f21c"
model:"claude-opus-5"
status:"completed"
usage:{ 3 items }
input:4182
output:663
cacheRead:3900
tools:[ 2 items ]
citations:[ 0 items ]
finishedAt:"2026-08-25T09:14:02Z"

Installation

Copy the source into your project, or keep it behind a package.

npx shadcn@latest add Tinkerers-Labs/mischief-ui/json-viewer
import { JsonViewer } from "mischief-ui/json-viewer"
Also installs
  • lucide-react

Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.

registry/default/json-viewer/json-viewer.tsx
"use client" import * as React from "react"import { Check, ChevronRight, Clipboard } from "lucide-react" import { cn } from "@/lib/utils" export type JsonViewerProps = Omit<  React.HTMLAttributes<HTMLDivElement>,  "children"> & {  value: unknown  rootName?: string  defaultExpandedDepth?: number

Usage

export function ToolResult({ payload }) {
  return (
    <JsonViewer
      value={payload}
      rootName="result"
      defaultExpandedDepth={2}
    />
  )
}

The path is the point

Reading a payload is half the job; the other half is saying where in it you were looking. Every row carries a copy control, and what it copies is the value, while the control names the path so the reader can see which one they are about to take.

Paths are written the way they would be typed back into code, so a key that cannot survive dot notation is bracketed and quoted instead of being silently mangled.

result.tools[0].input.query
result["content-type"]
A plain key, and one that needs brackets.

How much is open to begin with

A tree that arrives fully collapsed is one line, and one that arrives fully expanded is the wall of text the component exists to avoid. defaultExpandedDepth decides how far down the first view goes, and one level is usually enough to show the shape.

A branch that is closed still says how much is inside it, so its size is legible without opening it. An empty object or array is a leaf: there is nothing to disclose, so it offers no control that would do nothing.

Long strings

One long string should not decide the width of the panel. Strings past maxStringLength are cut with an ellipsis inside the quotes, and the copy control still yields the whole thing rather than what is shown.

API

valueunknownThe data. Anything JSON can hold.
rootNamestringWhat the top row is called, and the first segment of every path. Defaults to "root".
defaultExpandedDepthnumberHow many levels are open on arrival. Defaults to 1.
maxStringLengthnumberWhere a string is cut for display. Defaults to 120.
copyablebooleanShows the per-row copy control. Defaults to true.
labelstringThe tree's accessible name. Defaults to "JSON".

Accessibility

The rows are a real tree: role=tree on the container, role=treeitem with aria-level on each row, and aria-expanded on the ones that can open, so a screen reader announces depth and state rather than reading an indented list. Arrow keys move and fold the way a tree is expected to behave -- Right opens then descends, Left closes then climbs to the parent -- with Home and End for the ends and Enter or Space to toggle. Only one row is in the tab order, so the tree is a single stop rather than a hundred. A copy is confirmed through a live region, since the icon change alone is not announced.