# JSON Viewer

> A collapsible JSON tree with type colors, search that opens the matching branches, copy value or path, and virtualization for huge payloads.

- Type: React component (Data Display)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add json-viewer`
- Docs: https://wingo-ui.com/components/json-viewer

## Interaction

Click or tap a branch to open or close it: the chevron turns and small branches slide open. Collapsed objects show their first keys and arrays their first values, with a count. Type in the search box (or press Ctrl+F / ⌘F while inside the viewer) and every key or value that matches is highlighted, its branches open and the first hit scrolls to the middle; Enter and Shift+Enter or the arrows step through the hits and the counter reads 2 of 5. Escape clears the search, a second Escape returns to the tree. With a mouse, hovering a row shows Copy value and Copy path; on a phone a long press opens a sheet that repeats the value (and a date in local time) with the same actions plus Expand all from here. The toolbar expands or collapses everything, copies the whole JSON and downloads it as a dated .json file; below 512px of width it folds into a ⋯ menu. The tree is one Tab stop: Up and Down move, Right opens or steps into a branch, Left closes it or goes to the parent, Home and End jump, * opens every sibling, Enter toggles a branch or selects a value, c copies the value and Shift+C its path, and the context menu key opens the row's actions. Long strings stop at maxStringLength with a Show more button (Right and Left on the row show and hide the rest), arrays over 100 items open in pages of 100, and above 300 visible rows only what is on screen is rendered.

## Installation

```bash
npx wingo-ui@latest add json-viewer
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/json-viewer` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/json-viewer.tsx`

npm dependencies: `npm i @tanstack/react-virtual motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Action Sheet](https://wingo-ui.com/components/action-sheet) ([Markdown](https://wingo-ui.com/components/action-sheet.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Empty State](https://wingo-ui.com/components/empty-state) ([Markdown](https://wingo-ui.com/components/empty-state.md))
- [Search Input](https://wingo-ui.com/components/search-input) ([Markdown](https://wingo-ui.com/components/search-input.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [useClipboard](https://wingo-ui.com/components/use-clipboard) ([Markdown](https://wingo-ui.com/components/use-clipboard.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useLongPress](https://wingo-ui.com/components/use-long-press) ([Markdown](https://wingo-ui.com/components/use-long-press.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [File export](https://wingo-ui.com/components/file-export) ([Markdown](https://wingo-ui.com/components/file-export.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Text match](https://wingo-ui.com/components/text-match) ([Markdown](https://wingo-ui.com/components/text-match.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { JsonViewer, type JsonViewerLabels } from "@/components/ui/json-viewer"

const labels: Partial<JsonViewerLabels> = {
  search: "Search keys and values",
  noMatches: "Nothing found",
}

const webhook = {
  id: "evt_4912873366",
  account: "acct_12345674",
  created_at: "2026-09-24T16:38:12-04:00",
  status: "ok",
  invoice: { number: "2026-0412", customer: { name: "Acme Corp", tax_id: "12-3456740" } },
  warnings: [],
}

export function WebhookDetails() {
  return (
    <JsonViewer
      data={webhook}
      rootName="webhook"
      defaultExpandDepth={2}
      fileName="invoice-2026-0412"
      locale="en-US"
      labels={labels}
      onSelect={(path, value) => console.log(path, value)}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data` | `unknown` |  | Any JSON-compatible value. Dates, undefined, BigInt (123n), NaN and Infinity show as words or numbers, Maps and Sets as objects and arrays, circular references as [Circular → data.parent]. Strings that contain JSON are not parsed: pass the parsed value. undefined shows the empty state. |
| `rootName?` | `string \| false` | `""` | A key label on the root, like "payload". Empty (false) shows the root braces only. |
| `defaultExpandDepth?` | `number` | `1` | Levels open on mount: 0 keeps the root closed, 1 shows its keys. Deeper opening goes through the API (expandToDepth). |
| `expandedKeys?` | `string[]` |  | Controlled open branches as pathKey strings ("$", "$/invoice/lines", pages of long arrays as "$/items/~g100"). |
| `defaultExpandedKeys?` | `string[]` |  | Open branches on mount; replaces defaultExpandDepth when set. |
| `onExpandedKeysChange?` | `(keys: string[]) => void` |  | Every open or close, including the branches a search opens. |
| `searchable?` | `boolean` | `true` | The search box in the toolbar. Without it the toolbar shows the root name and its count. |
| `searchIn?` | `"keys" \| "values" \| "both"` | `"both"` | What the search matches. Values match as their text (numbers, true, null too); case and diacritics are ignored. |
| `toolbar?` | `boolean` | `true` | The row with search, expand / collapse all, copy and download. |
| `pathFormat?` | `"js" \| "pointer"` | `"js"` | Copy path as data.items[0].name (keys with spaces as ["full name"]) or as a JSON Pointer /items/0/name. |
| `rootPathName?` | `string` | `"data"` | The root identifier in js paths: data.invoice.customer. |
| `maxStringLength?` | `number` | `120` | Longer strings stop with an ellipsis and a Show more button. A search hit further in shows the whole string. |
| `showCounts?` | `boolean` | `true` | 3 keys / 12 items after objects and arrays. |
| `showTypes?` | `boolean` | `false` | A small type chip before each value: string, int, float, bool, null, date, object, array. |
| `showLineNumbers?` | `boolean` | `false` | A sticky gutter with row numbers that stays put while long lines scroll sideways. |
| `quoteKeys?` | `boolean` | `true` | "name": like JSON, or name: without quotes. |
| `sortKeys?` | `boolean` | `false` | Shows object keys alphabetically. Display only: copy and download keep the original order. |
| `detectDates?` | `boolean` | `true` | ISO date strings get the date color (still quoted) and a native tooltip with the local date and time. |
| `size?` | `"sm" \| "md"` | `"md"` | 12px text on 24px rows or 13px on 28px rows. Rows grow to 36px on touch screens. |
| `height?` | `number` | `0` | A fixed body height in px. 0 grows with the content up to maxHeight. |
| `maxHeight?` | `number` | `480` | The tallest the body grows when height is 0; it scrolls inside after that. |
| `virtualThreshold?` | `number` | `300` | Visible rows above which only the rows on screen are rendered (each row measured, 12 rows of overscan). |
| `wrap?` | `"auto" \| boolean` | `"auto"` | Long values break onto more lines, indented under their key, instead of scrolling sideways. auto wraps below 512px of the viewer's width (phones). |
| `onSelect?` | `(path: JsonPath, value: unknown) => void` |  | A value activated with Enter, a click or a tap. With it set, the selected row stays highlighted. |
| `fileName?` | `string` | `"data"` | Download name base; the file is saved as data-2026-09-24-1640.json. |
| `downloadable?` | `boolean` | `true` | The Download action in the toolbar. |
| `copyable?` | `boolean` | `true` | The Copy JSON action in the toolbar (indented, circular references and BigInts made safe). |
| `rowActions?` | `boolean` | `true` | Copy value and Copy path on each row: on hover or focus with a mouse, on long press on touch screens. |
| `loading?` | `boolean` | `false` | Skeleton rows in place of the tree; the toolbar actions wait. |
| `skeletonCount?` | `number` | `8` | Skeleton rows while loading. |
| `emptyState?` | `ReactNode` |  | Replaces the built-in No data state shown when data is undefined. |
| `setApi?` | `(api: JsonViewerApi) => void` |  | Called after mount (and when the API changes) with { expandAll, collapseAll, expandToDepth(depth), expandPath(path), scrollToPath(path), search(query) }. |
| `locale?` | `string` | `""` | Counts and date tooltips. Empty uses the UIProvider locale, then the browser's. |
| `labels?` | `Partial<JsonViewerLabels>` |  | Every string: search, matches(i, n), noMatches, previousMatch, nextMatch, expandAll, collapseAll, copy, copyValue, copyPath, download, copied, copyFailed, downloaded(name), items(count, formatted), keys(count, formatted), more(count, formatted), less, expand, collapse, expandHere, moreActions, cancel, circular, empty, loading, tree. English defaults in DEFAULT_JSON_VIEWER_LABELS. |
| `classNames?` | `Partial<Record<"toolbar" \| "summary" \| "search" \| "body" \| "row" \| "lineNumber" \| "toggle" \| "key" \| "value" \| "type" \| "preview" \| "count" \| "actions" \| "more" \| "empty", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the root panel, merged last. Give it a height (h-full) and height={0} with maxHeight={0} to fill a pane. |

## Parts

### formatJsonPath (function)

The path text copy path uses: formatJsonPath(["items", 0, "name"]) is data.items[0].name, with "pointer" /items/0/name. Signature: (path: JsonPath, format?: "js" | "pointer", rootName?: string) => string.

### pathKey (function)

The key expandedKeys uses for a path: pathKey(["invoice", "lines"]) is "$/invoice/lines". Signature: (path: JsonPath) => string.

### flattenJson (function)

The visible rows (nodes, array pages and closing braces) for a set of open keys, for tests and custom renderers. Signature: (value: unknown, expandedKeys: Iterable<string>, options?: { rootName?; sortKeys?; detectDates? }) => JsonRow[].

### JSON_COLORS (constant)

The value color classes, shared with anything that prints JSON inline (log metadata). Type: Record<"key" | "string" | "number" | "boolean" | "null" | "date" | "punctuation", string>.

### jsonTypeOf (function)

The node type the viewer colors a value by: object, array (Sets too), string, number, boolean, null, undefined, or date for Date objects and ISO date strings. Signature: (value: unknown, detectDates = true) => JsonNodeType.

### DEFAULT_JSON_VIEWER_LABELS (constant)

Every English string the viewer prints or announces (search, match counts, copy and download feedback, item counts, menu entries). Spread it and override what you translate.

### JsonPath (type)

A path into the data as keys and indexes, e.g. ["items", 0, "name"]. onSelect and the copy actions use it.

### JsonViewerLabels (type)

The shape of labels; plain strings plus functions for counts like items(count, formatted).

## Examples

- **Search**: Hits in keys and values light up, their branches open and the counter reads 1 of 2.
- **10,000 rows, virtualized**: Long arrays open in pages of 100 and only the visible rows are in the DOM.
- **Select a value**: onSelect with JSON Pointer paths feeding a side panel.
- **Tool call input**: size sm with type chips and line numbers inside an AI tool call card.
- **Long strings**: A base64 PDF truncated at 120 characters with a show more button.
- **On a phone**: Long press a row for an action sheet; the toolbar folds into a menu.
- **Loading and empty**: Skeleton rows while the payload loads, and the empty state without data.

## See also

- [Tree View](https://wingo-ui.com/components/tree-view) ([Markdown](https://wingo-ui.com/components/tree-view.md))
- [Code Block](https://wingo-ui.com/components/code-block) ([Markdown](https://wingo-ui.com/components/code-block.md))
- [Copy Button](https://wingo-ui.com/components/copy-button) ([Markdown](https://wingo-ui.com/components/copy-button.md))
- [Search Input](https://wingo-ui.com/components/search-input) ([Markdown](https://wingo-ui.com/components/search-input.md))
- [File export](https://wingo-ui.com/components/file-export) ([Markdown](https://wingo-ui.com/components/file-export.md))

---

Canonical: https://wingo-ui.com/components/json-viewer
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
