@juxtapos/json-viewer
v0.1.0
Published
Collapsible JSON tree viewer (React component + framework-free <json-viewer> custom element) with find, JSONPath rules, virtualization for large documents, keyboard navigation, and full custom-property theming.
Maintainers
Readme
This component was created because I wanted this exact feature set that I could reuse throughout my projects:
- Collapsible JSON view with smooth transitions. I strongly dislike janky UIs.
- Keyboard navigation
- Custom formatting based on JSONPath.
- Eventing for each selection.
- Custom theming
Install
npm install @juxtapos/json-viewerReact 18+ is a peer dependency.
Usage
import { JsonViewer } from '@juxtapos/json-viewer'
import '@juxtapos/json-viewer/styles.css'
export function Example() {
return <JsonViewer value={{ hello: 'world', items: [1, 2, 3] }} />
}The viewer expects to live inside a flex column with a fixed height — give
the parent display: flex; flex-direction: column and let the viewer
flex-grow so its body scrolls independently.
View rules
A rule decorates matched nodes with custom styles and turns clicked
primitives into actionable links. Paths are full JSONPath expressions
(via jsonpath-plus):
const rules: ViewRule<MyAction>[] = [
{
id: 'highlight-ids',
name: 'Highlight user IDs',
enabled: true,
target: '$..userId',
action: { type: 'open-user' },
style: 'color: orange; font-weight: 600',
},
]target is a JSONPath expression evaluated via jsonpath-plus. The
leading $ is optional — users[*].id and $.users[*].id are
equivalent. Matched primitives become clickable and call
onRuleAction(rule, value, rawValue).
Custom rendering
A rule can replace the default rendering for nodes it matches by
supplying render. Use it for hex-color swatches, image previews,
mailto links, formatted timestamps — anything the default "value"
chrome is wrong for.
const rules: ViewRule<MyAction>[] = [
{
id: 'r-color',
name: 'Hex color swatch',
enabled: true,
target: '$..color',
action: { type: 'noop' },
render: ({ value }) =>
typeof value === 'string' && /^#[0-9a-f]{6}$/i.test(value) ? (
<span>
<span
style={{
display: 'inline-block', width: 10, height: 10,
background: value, marginRight: 4, borderRadius: 2,
}}
/>
"{value}"
</span>
) : null,
},
]The renderer receives { value, path, rule }. Return null (or false)
to fall back to the default rendering — useful when the renderer only
wants to handle some matched values (e.g. valid hex strings) and leave
the rest alone. When multiple matched rules supply render, the first
one wins.
Custom element (no framework required)
The viewer also ships as a self-contained HTML custom element — React is
bundled inside dist/element.js as an implementation detail, so the host
page needs no framework:
<script type="module" src=".../@juxtapos/json-viewer/dist/element.js"></script>
<json-viewer toolbar style="height: 400px"></json-viewer>
<script>
const el = document.querySelector('json-viewer')
el.value = { hello: 'world', items: [1, 2, 3] }
el.addEventListener('jv-selection-change', (e) => console.log(e.detail.path))
</script>Or from a bundler: import '@juxtapos/json-viewer/element'.
Properties — value, viewRules, sortKeys, expandAll,
virtualizeThreshold, selectedPath (assign to drive selection),
toolbar, expandState (read-only Map).
Attributes — value (JSON string), sort-keys, expand-all,
virtualize-threshold, selected-path, toolbar.
Events (bubbling, composed) — jv-selection-change {path},
jv-rule-action {rule, value, rawValue}, jv-node-contextmenu
{path, value, rawValue, clientX, clientY}, jv-expand-change
{path, expanded, recursive} (or null when the viewer auto-expanded
ancestors to reveal a node).
View rules work unchanged, except render is framework-neutral: return
an HTML string or a DOM Node instead of a React node.
The element renders into an open shadow root. Theming still works — set
the --jv-* custom properties on the element or any ancestor; they
inherit through the shadow boundary. Give the element a bounded height
(it's a display: flex column host all by itself).
React hosts should keep using the JsonViewer component from the main
entry — it's a fraction of the size and shares the host's React.
Large documents
Above 3000 total nodes the viewer automatically switches from the nested, animated DOM to a virtualized renderer: the visible tree is flattened into fixed-height rows and only the rows in (and just around) the viewport are mounted. Find, JSONPath rules, custom renderers, keyboard navigation, selection, and copy all behave identically — the only difference is that expand/collapse is instant instead of animated.
Tune the switchover with virtualizeThreshold:
<JsonViewer value={huge} virtualizeThreshold={0} /> // always virtualize
<JsonViewer value={data} virtualizeThreshold={Infinity} /> // never virtualizeIn virtualized mode the viewer must have a bounded height (the flex-column
layout described above already provides one). Row height is fixed and read
from the --jv-row-height custom property (default 21px) — if you change
the font size, set a matching row height.
Theming
Every color the viewer renders is exposed as a CSS custom property scoped
to the viewer root (.jv-default). Override on any ancestor to retheme:
.jv-default {
--jv-text-string: #ff7;
--jv-text-number: #7df;
--jv-bg-selected: rgba(74, 158, 255, 0.15);
}The full token list is documented at the top of dist/styles.css.
Keyboard
- ↑ / ↓ — move between siblings at the same level; falls off the edge to the parent or its next sibling.
- → — expand the focused node, then descend into its children.
- ← — move to the parent node. Never collapses — leaving a node keeps it open.
- Enter / Space — collapse or expand the focused node (with Alt: recursively, like alt-click).
- Cmd / Ctrl + F — open find; supports substring or
$JSONPath expressions. Scoped to the viewer: it only fires while focus is inside the component, so the browser's own find-in-page keeps working elsewhere. - Cmd / Ctrl + C — copy the selected node as JSON (or the highlighted text if any).
License
MIT — see LICENSE.
