API Reference
Complete documentation for the <json-viewer> custom element. Tag name: json-viewer. Class: JsonViewer (extends HTMLElement).
- Installation
- Attributes
- Properties
- Methods
- Events
- Path grammar
- CSS custom properties
- CSS parts
- Examples
Installation
npm
npm install @profpowell/json-viewer
Then in your module:
import '@profpowell/json-viewer' // side-effect import: defines <json-viewer>
// or
import { JsonViewer } from '@profpowell/json-viewer'
customElements.define('my-viewer', JsonViewer)
CDN
<script type="module" src="https://unpkg.com/@profpowell/json-viewer"></script>
No bundler
Drop dist/json-viewer.js next to your HTML and reference it directly:
<script type="module" src="./json-viewer.js"></script>
<json-viewer expanded="2"></json-viewer>
Attributes
All attributes are observed: changing them after the element is connected triggers a re-render.
| Attribute | Type | Default | Description |
|---|---|---|---|
data |
JSON string | โ | Inline JSON data. Prefer the .data property for non-stringifiable values (Maps, BigInts, etc.). |
src |
URL | โ | Fetched as JSON on connect and whenever the attribute changes. Errors render as { error: "<message>" }. |
expanded |
int | true | false |
1 |
Initial expansion depth. 0 opens the root only; integers open that many levels; true opens everything; false opens nothing. |
mode |
tree | raw |
tree |
raw mode renders a pretty-printed JSON string in a <pre> block instead of the interactive tree. |
show-types |
boolean | false |
When present, type chips (string, number, array, โฆ) appear next to each value. |
show-sizes |
boolean | false |
When present, array length and object/Map/Set entry counts appear in the container preview. |
show-indices |
boolean | false |
When present, array indices render as a leading 0:, 1:, โฆ prefix on each item. |
indent |
int (ch units) | 2 |
Indentation width per level (in ch). Also controls the JSON indent in raw mode. |
Properties
| Property | Type | Description |
|---|---|---|
.data |
unknown |
Read/write. Setting triggers a re-render and pre-builds the search-record index. Accepts any JS value โ primitive, plain object, array, Map, Set, Date, RegExp, BigInt, Function, or circular graph. |
Methods
.expand(matcher) / .collapse(matcher)
Open or close every rendered container whose path matches matcher.
| Matcher type | Example | Behavior |
|---|---|---|
| string (exact) | 'users[0].name' |
Matches the exact path, or any path ending in .users[0].name / ]users[0].name. |
| string (glob) | 'users[*].roles' |
* matches one path segment; ** matches zero or more. |
| RegExp | /^users\[\d+\]\.email$/ |
Tested against the full path string. |
| function | p => p.startsWith('config') |
Called with each rendered path; truthy result means a match. |
.expandAll() / .collapseAll()
Open or close every currently-rendered container.
.search(query, options?)
Returns a navigable iterator over matches.
const it = viewer.search('error', { regex: false, caseSensitive: false })
it.count // total matches
it.current?.path // path of the focused match, or undefined
it.next() // advance + return { value, done }
it.prev() // step back
// also iterable: for (const m of viewer.search('foo')) { ... }
The search walks an internal record array built from .data, not the DOM. Only the ancestors of the focused match are expanded โ never the whole tree. Only the focused match is wrapped in <mark>.
Options:
| Option | Default | Description |
|---|---|---|
regex | false | Treat query as a regular expression source. |
caseSensitive | false | Match case exactly. |
.entryAt(path)
Returns { key, value } for any path, including Map and Set entries where the key isn't a string. For object/array paths, key is the last segment; for Map entries it's the original key object (reference-equal to what you put in); for Set entries it's the integer index.
const adaKey = { id: 1 }
const cache = new Map([[adaKey, { hits: 42 }]])
viewer.data = { cache }
const entry = viewer.entryAt('cache@0')
entry.key === adaKey // true โ reference preserved
entry.value.hits // 42
.copy(text?)
Writes to the clipboard. With no argument, copies a BigInt-safe JSON snapshot of .data. With an argument, copies that string verbatim. Returns a Promise.
await viewer.copy() // full BigInt-safe JSON
await viewer.copy(viewer.entryAt('users[0]').value.email)
Events
Both events bubble and are composed: true so they cross the Shadow DOM boundary.
| Event | detail | Fires when |
|---|---|---|
json-viewer:keyclick |
{ path, key, value } |
A key (the quoted property name in the tree) is clicked. The path-bar in the toolbar also opens. |
json-viewer:valueclick |
{ path, value, type } |
A primitive value (string, number, etc.) is clicked. type is the result of internal kindOf(): string, number, boolean, null, undefined, bigint, symbol, function, date, regexp, array, object, map, set. |
Path grammar
Paths are flat strings. The grammar:
| Segment | Example | Meaning |
|---|---|---|
| Object key (identifier) | users | Property access. Joined with . for nested keys. |
| Object key (special) | ["weird-key"] | Bracket-quoted when the key isn't a valid JS identifier. |
| Array index | users[0] | Zero-based index in brackets. |
| Map / Set entry | cache@0 | Insertion-order index, prefixed with @. Use .entryAt to recover the original key. |
| Root | $ | The whole value. |
Globs:
| Pattern | Matches |
|---|---|
* | Exactly one path segment (no dots or brackets inside). |
** | Zero or more path segments โ used at the start or end of a glob, e.g. **.email. |
CSS custom properties
Public theme contract. Each token resolves through a three-level fallback chain:
var(--json-viewer-FOO, var(--vb-FOO, light-dark(LIGHT, DARK)))
Set --json-viewer-* for direct overrides, set --vb-* at the page level for ecosystem-wide theming, or leave both unset for the built-in light-dark() defaults.
| Public token | VB fallback | Default (light / dark) |
|---|---|---|
--json-viewer-font-mono | --vb-font-mono | system mono stack |
--json-viewer-surface | --vb-surface | #ffffff / #1c1917 |
--json-viewer-text | --vb-text | #1c1917 / #f4f4f5 |
--json-viewer-text-muted | --vb-text-muted | #71717a / #a1a1aa |
--json-viewer-border | --vb-border | #e7e5e4 / #3f3f46 |
--json-viewer-accent | --vb-accent | #0f766e / #2dd4bf |
--json-viewer-radius | --vb-radius | 0.5rem |
--json-viewer-space-1..4 | --vb-space-1..4 | 0.25 / 0.5 / 0.75 / 1 rem |
--json-viewer-key | --vb-json-key | #7e22ce / #c084fc |
--json-viewer-string | --vb-json-string | #15803d / #4ade80 |
--json-viewer-number | --vb-json-number | #1d4ed8 / #60a5fa |
--json-viewer-boolean | --vb-json-boolean | #a16207 / #fbbf24 |
--json-viewer-null | --vb-json-null | #71717a / #a1a1aa |
--json-viewer-punct | --vb-json-punct | #52525b / #a1a1aa |
CSS parts
Use ::part() to target inner regions for layout-level customization:
| Part | Description |
|---|---|
toolbar | The top action bar (Expand / Collapse / Search / Raw / Copy). |
search | The search panel (input + next/prev/close buttons). |
path-bar | The path-display strip that appears on key click. |
path | The <code> element inside the path-bar. |
body | The scrollable container holding the tree (or the raw <pre>). |
Example: borderless embed
json-viewer::part(toolbar) { display: none; }
json-viewer { border: none; }
Examples
Set data after fetching
const viewer = document.querySelector('json-viewer')
viewer.data = await fetch('/api/me').then(r => r.json())
React to clicks
viewer.addEventListener('json-viewer:keyclick', (e) => {
console.log('clicked', e.detail.path, '=', e.detail.value)
})
Expand specific paths after a search miss
const it = viewer.search('errorCode')
if (it.count === 0) {
viewer.expand('**.errors') // expose error sections so a follow-up search hits them
}
Theme a single instance
<json-viewer style="--json-viewer-accent: #ff5722; --json-viewer-radius: 0">
</json-viewer>