caliper uidocs

JSON viewer

Free

A collapsible tree of JSON with copy and search.

Installs components/ui/json-viewer.tsxDepends on @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/json-viewer.json

Install a component

bash
pnpm dlx caliper add table tree-view# adds the source under components/ui

Use it in a page

accounts.tsx
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} />    </>  );}

tokens.css
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;  }}

response.json
json
{  "ok": true,  "items": [    1,    2,    3  ],  "next": null}

Inspect a webhook payload

invoice.payment_succeeded
  • { 7 keys }
  • id"evt_1Q9x"
  • object"event"
  • livemodefalse
  • created1790000000
  • type"invoice.payment_succeeded"
$

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.