JSON viewer
FreeA collapsible tree of JSON with copy and search.
pnpm dlx shadcn@latest add https://caliperui.site/r/json-viewer.jsonInstall a component
bash
pnpm dlx caliper add table tree-view# adds the source under components/uiUse it in a page
tsx
import { SortableTable, FilterToolbar } from "@/components/ui";
type Account = { id: string; name: string; mrr: number };
const columns = [ { id: "name", header: "Account", cell: (r: Account) => r.name, sortValue: (r: Account) => r.name }, { id: "mrr", header: "MRR", align: "end" as const, cell: (r: Account) => `$${r.mrr}`, sortValue: (r: Account) => r.mrr },];
export function Accounts({ rows }: { rows: Account[] }) { const [query, setQuery] = useState(""); const visible = rows.filter((r) => r.name.toLowerCase().includes(query.toLowerCase()));
return ( <> <FilterToolbar label="Accounts" query={query} onQueryChange={setQuery} filters={[]} value={{}} onValueChange={() => {}} /> {/* Sorting, selection and empty states are handled by the table. */} <SortableTable caption="Accounts" columns={columns} rows={visible} getRowId={(r) => r.id} /> </> );}css
:root { --accent: #1f6f78; --duration-travel: 280ms; /* bounded movement */ --ease-showcase: cubic-bezier(0.22, 1, 0.36, 1);}
@media (prefers-reduced-motion: reduce) { [data-slot="tree-panel"] { transition: none; }}json
{ "ok": true, "items": [ 1, 2, 3 ], "next": null}When to use #
Use it for
- Inspecting an API response or payload.
Skip it when
- The data is not JSON.
Installation #
terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/json-viewer.json
Adds its 5 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
example.tsx
import { useState } from "react" import { CodeBlock } from "@/components/ui/code-block" import { JsonViewer } from "@/components/ui/json-viewer" import { Label } from "@/components/ui/label" import { Search } from "@/components/ui/search" const install = [ { id: "pnpm", label: "pnpm", code: "pnpm dlx caliper add table tree-view\n# adds the source under components/ui", language: "bash" as const }, { id: "npm", label: "npm", code: "npx caliper add table tree-view\n# adds the source under components/ui", language: "bash" as const }, { id: "yarn", label: "yarn", code: "yarn dlx caliper add table tree-view\n# adds the source under components/ui", language: "bash" as const }, ]; const component = `import { SortableTable, FilterToolbar } from "@/components/ui"; type Account = { id: string; name: string; mrr: number }; const columns = [ { id: "name", header: "Account", cell: (r: Account) => r.name, sortValue: (r: Account) => r.name }, { id: "mrr", header: "MRR", align: "end" as const, cell: (r: Account) => \`$\${r.mrr}\`, sortValue: (r: Account) => r.mrr }, ]; export function Accounts({ rows }: { rows: Account[] }) { const [query, setQuery] = useState(""); const visible = rows.filter((r) => r.name.toLowerCase().includes(query.toLowerCase())); return ( <> <FilterToolbar label="Accounts" query={query} onQueryChange={setQuery} filters={[]} value={{}} onValueChange={() => {}} /> {/* Sorting, selection and empty states are handled by the table. */} <SortableTable caption="Accounts" columns={columns} rows={visible} getRowId={(r) => r.id} /> </> ); }`; const tokens = `:root { --accent: #1f6f78; --duration-travel: 280ms; /* bounded movement */ --ease-showcase: cubic-bezier(0.22, 1, 0.36, 1); } @media (prefers-reduced-motion: reduce) { [data-slot="tree-panel"] { transition: none; } }`; const events = Array.from({ length: 120 }, (_, i) => ({ id: `evt_${1000 + i}`, type: i % 7 === 0 ? "charge.failed" : "charge.succeeded", amount: 1900 + i * 25 })); const payload = { id: "evt_1Q9x", object: "event", livemode: false, created: 1790000000, type: "invoice.payment_succeeded", data: { object: { id: "in_0042", customer: { id: "cus_81", email: "billing@northwind.example", address: { city: "Leeds", country: "GB", line2: null } }, lines: [ { description: "Team plan, 14 seats", amount: 28000, proration: false }, { description: "Priority support", amount: 9900, proration: false }, ], paid: true, attempt_count: 1, notes: "Thanks for being a customer. This invoice covers October and includes the annual support add-on you requested during onboarding, plus a credit applied from the September billing correction.", metadata: { order_ref: "NW-2210", tags: ["priority", "renewal"] }, }, }, recent: events, }; export function JsonViewerDemo() { const [query, setQuery] = useState(""); return ( <div className="grid min-w-0 gap-8" data-testid="source-viewer-examples"> <div className="grid min-w-0 gap-3"> <h3 className="text-sm font-medium">Install a component</h3> <CodeBlock variants={install} label="Install command" /> </div> <div className="grid min-w-0 gap-3"> <h3 className="text-sm font-medium">Use it in a page</h3> <CodeBlock title="accounts.tsx" language="tsx" code={component} lineNumbers highlight={[14, 15, 16]} collapsedLines={12} /> </div> <div className="grid min-w-0 gap-3 sm:grid-cols-2"> <CodeBlock title="tokens.css" language="css" code={tokens} wrap /> <CodeBlock title="response.json" language="json" code={JSON.stringify({ ok: true, items: [1, 2, 3], next: null }, null, 2)} /> </div> <div className="grid min-w-0 gap-3 rounded-overlay border border-border p-4 sm:p-5"> <h3 className="text-sm font-medium">Inspect a webhook payload</h3> <div className="grid gap-1.5"> <Label htmlFor="json-search">Find a key or value</Label> <Search id="json-search" placeholder="Try northwind or amount" value={query} onChange={(e) => setQuery(e.target.value)} onClear={() => setQuery("")} /> </div> <JsonViewer label="invoice.payment_succeeded" data={payload} query={query} /> </div> </div> ); }
API reference #
JsonViewer
component
PropTypeDefaultDescription
label *string—
data *unknown—
defaultDepthnumber1Levels open at first.
querystring""Highlights matching keys and values and opens the branches that hold them.
classNamestring—
Accessibility #
It exposes tree, treeitem and status semantics, with aria-label, aria-level, aria-setsize, aria-posinset, aria-expanded and aria-selected kept in step with state. Changes that matter are announced through a live region.
KeyBehaviour
↓Moves to the next item or value.
↑Moves to the previous item or value.
HomeJumps to the first item or the minimum.
EndJumps to the last item or the maximum.
→Moves to the next item or value.
←Moves to the previous item or value.
EnterActivates the focused control.
SpaceActivates the focused control.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.