npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@binaryoperations/json-tree-editor

v2.0.1

Published

Interactive JSON tree editor — Solid components + framework-agnostic web component

Readme

@binaryoperations/json-tree-editor

Interactive JSON tree editor you can drop into any app: edit keys, types, and primitives in a collapsible tree while treating a JSON string as the single source of truth.

Use it as a SolidJS component (TypeScript source, peer solid-js) or as a framework-agnostic web component (<json-tree-editor> with Solid bundled).

Highlights

  • Collapsible object/array tree with type changes, rename, add/delete, clear, duplicate
  • Controlled value / onChange (pretty JSON string)
  • Optional plugins (e.g. path-scoped history / undo-redo)
  • In-tree search (Cmd/Ctrl+F), optional array drag-reorder, read-only mode
  • CSS variables + ::part theming

Install

npm install @binaryoperations/json-tree-editor
# or: pnpm add @binaryoperations/json-tree-editor
# or: yarn add @binaryoperations/json-tree-editor

Solid apps also need solid-js and a Solid toolchain (for example vite-plugin-solid):

npm install solid-js

Package entry points

| Import | What you get | | --- | --- | | @binaryoperations/json-tree-editor | JsonTreeView + props/handle types (peer solid-js) | | @binaryoperations/json-tree-editor/plugin | definePlugin + plugin contract types | | @binaryoperations/json-tree-editor/history | Path-scoped undo/redo — opt-in (history README) | | @binaryoperations/json-tree-editor/breadcrumbs | Path bar + selectPath — opt-in (breadcrumbs README) | | @binaryoperations/json-tree-editor/dnd | Array drag-and-drop (HTML5_ARRAY_REORDER, …) — opt-in | | @binaryoperations/json-tree-editor/utils | Parse helpers, path utilities, lower-level primitives | | @binaryoperations/json-tree-editor/web-component | Custom element class (no auto-register) | | @binaryoperations/json-tree-editor/web-component/register | Defines <json-tree-editor> on import (Solid bundled; DnD on by default) | | @binaryoperations/json-tree-editor/styles.css | Structure only (also inlined into the WC shadow) | | @binaryoperations/json-tree-editor/themes/*.css | A complete look — each file @imports structure. Import exactly one. |


Quick start

import { createSignal } from 'solid-js';
import { JsonTreeView } from '@binaryoperations/json-tree-editor';
import '@binaryoperations/json-tree-editor/themes/default.css';

export function JsonPanel() {
  const [source, setSource] = createSignal('{"hello":"world"}');

  return (
    <JsonTreeView
      value={source()}
      onChange={(prettyJson) => setSource(prettyJson)}
    />
  );
}
<script type="module">
  import '@binaryoperations/json-tree-editor/web-component/register';
  import '@binaryoperations/json-tree-editor/themes/default.css';

  const el = document.querySelector('json-tree-editor');
  el.value = JSON.stringify({ hello: 'world', count: 1 }, null, 2);

  el.addEventListener('change', (e) => {
    console.log(e.detail.value); // pretty JSON string
  });
</script>

<json-tree-editor></json-tree-editor>
import '@binaryoperations/json-tree-editor/web-component/register';
import '@binaryoperations/json-tree-editor/themes/default.css';
import type { JsonTreeEditorElement } from '@binaryoperations/json-tree-editor/web-component';

const el = document.querySelector('json-tree-editor') as JsonTreeEditorElement;
el.value = '{"name":"Ada"}';

el.addEventListener('change', (event) => {
  const { value } = (event as CustomEvent<{ value: string }>).detail;
  console.log(value);
});

SolidJS API

Import one theme in your app entry or layout (it pulls in structure):

import '@binaryoperations/json-tree-editor/themes/default.css';

JsonTreeView props

| Prop | Type | Required | Description | | --- | --- | --- | --- | | value | string | yes | JSON document source (parsed internally) | | onChange | (prettyJson: string) => void | yes | Called after an edit with pretty JSON (2-space indent) | | defaultExpandedDepth | number | no | Nesting levels open on mount (0 = root only, default) | | arrayReorder | ArrayReorderController \| false | no | Array drag-reorder. Omit / false = off. Pass HTML5_ARRAY_REORDER from /dnd | | plugins | JsonTreeEditorPlugin[] | no | Editor plugins (e.g. historyPlugin()). Stable by plugin name | | readOnly | boolean | no | Browse/expand only; no mutations | | search | boolean | no | In-tree find (Cmd/Ctrl+F). Default true | | ref | handle or callback | no | Imperative API (see below) |

Ref handle

| Method | Description | | --- | --- | | getRoot() | Root .json-tree DOM element (or null before mount) | | use(plugin) | Register a plugin; returns dispose | | callCommand(name, …args) | Invoke a registered command (e.g. history undo) | | hasCommand(name) | Whether a command is registered |

Keep the source string as document truth: pass it as value, push tree edits back via onChange.

Root rules (applied inside the view): blank source → valid empty object {}. Root must be an object or array. Syntax errors keep a last-good tree visible with an error banner; tree edits still emit pretty JSON so you can recover.


Plugins

Plugins are opt-in modules that observe document transactions, register commands, and may contribute UI. Core stays free of undo stacks / CRDTs / chrome.

| Surface | How | | --- | --- | | Solid | plugins={[myPlugin()]} and/or handle.use(myPlugin()) | | Web component | el.plugins = […] and/or el.use(plugin) (queues until connected) | | Commands | handle.callCommand('…') / el.callCommand('…') |

Command registry: first registrant is master for a command name; later plugins are subordinates. Types: @binaryoperations/json-tree-editor/plugin. Architecture: plugin system PRD.

Prefer stable plugin instances. Identity is by name, and a changed list reinstalls the set; hoisting the array to a const keeps that from happening on unrelated re-renders. (An inline plugins={[myPlugin()]} is read once and cached, so it works — it is just easier to churn by accident.)

The contract

type JsonTreeEditorPlugin = {
  name: string;
  /** Slot for `render`. Default `'head'`. Ignored without `render`. */
  stage?: 'head' | 'tail';
  setup(ctx: PluginContext): void | (() => void);
  /** Optional UI. Called once, in the slot named by `stage`. */
  render?(stage: 'head' | 'tail', ctx: PluginContext): JSX.Element | null;
};

render — plugin UI

A plugin that needs chrome (a path bar, a status strip, a toolbar) returns it from render instead of asking the host to place a component.

  • Slots. stage: 'head' (default) mounts above everything — before the error banner, the find bar and the tree. stage: 'tail' mounts below the tree scroller.
  • Called once per plugin, in its own slot. The stage argument is passed so one component can serve both placements; it is not an invitation to render twice.
  • Reactive context. render runs inside the view's reactive root, so Solid primitives are safe and cleanup runs on unmount. Hold plugin state in signals created by the factory and written from setup.
  • Isolated. A throwing render is logged and skipped — it cannot blank the tree.
  • Solid hosts only. JsonTreeView and the web component render it; headless hosts ignore it. Return null to contribute nothing.

Plugins that render usually also need the view's DOM mechanics. Those are published as commands, and are registered only once at least one plugin is installed:

| Command | Signature | Does | | --- | --- | --- | | json-tree.expandPath | (path) => true | Expand every ancestor of path (and path itself when it is a container) | | json-tree.revealPath | (path, { focus? }) => Promise<HTMLElement \| null> | Scroll the row into view once it exists and optionally focus it. Resolves the row element — so a plugin can decorate it — or null if it never appears | | json-tree.getFocusedPath | () => JsonPath | The roving-tabindex row | | json-tree.onFocusedPathChange | (cb) => unsubscribe | Fires when the focused row changes |

The split is deliberate: the view owns what needs its internals (expand state, sticky-aware scrolling) and hands back the element. Everything else — navigation policy, decoration — stays in the plugin. selectPath and its flash ring are composed from these primitives by the breadcrumbs plugin, not built in.

setup runs untracked, so reading editor state there does not subscribe the host's install effect to it.

import { definePlugin } from '@binaryoperations/json-tree-editor/plugin';

export function statusPlugin() {
  const [count, setCount] = createSignal(0);

  return definePlugin({
    name: 'status',
    stage: 'tail',
    setup(ctx) {
      return ctx.onTransaction(() => setCount((n) => n + 1));
    },
    render(_stage, ctx) {
      return <footer>{count()} edits · {ctx.getState().validity.ok ? 'valid' : 'invalid'}</footer>;
    },
  });
}

Breadcrumbs plugin

Import: @binaryoperations/json-tree-editor/breadcrumbs

A path bar for the focused row, and the selectPath command — expand ancestors, scroll the row into view, flash a ring around its key.

→ Full docs: src/breadcrumbs/README.md

import { breadcrumbsPlugin } from '@binaryoperations/json-tree-editor/breadcrumbs';

const plugins = [breadcrumbsPlugin()];

<JsonTreeView value={json()} onChange={setJson} plugins={plugins} />
// The bar is rendered by the plugin — no extra markup.
await handle.callCommand('selectPath', ['meta', 'author', 'email']);

History plugin

Import: @binaryoperations/json-tree-editor/history

Path-scoped undo/redo — subtree deltas on the stack, not full-document copies.

→ Full docs: src/history/README.md
Design: history PRD.

import { JsonTreeView } from '@binaryoperations/json-tree-editor';
import { historyPlugin } from '@binaryoperations/json-tree-editor/history';

const plugins = [historyPlugin({ maxDepth: 50 })];

<JsonTreeView value={json()} onChange={setJson} plugins={plugins} />
// handle.callCommand('undo' | 'redo' | 'canUndo' | 'readHistory' | …)
import '@binaryoperations/json-tree-editor/web-component/register';
import '@binaryoperations/json-tree-editor/themes/default.css';
import { historyPlugin } from '@binaryoperations/json-tree-editor/history';

const el = document.querySelector('json-tree-editor')!;
el.use(historyPlugin({ maxDepth: 50 }));
el.callCommand('undo');

Web component API

| Surface | Type | Notes | | --- | --- | --- | | Property value | string | Preferred source of truth (esp. large JSON) | | Attribute value | string | Optional; reflected only when length ≤ ~8KB | | defaultExpandedDepth / default-expanded-depth | number | Nesting levels open on mount (default 0) | | readOnly / readonly | boolean | Browse/expand only | | arrayReorder / array-reorder | boolean | Array DnD (default on for WC). Set false to disable | | search | boolean | In-tree find (default on) | | Property plugins | JsonTreeEditorPlugin[] | Replace plugin set (no HTML attribute) | | Method use(plugin) | () => void dispose | Register one plugin (pre-connect queue OK) | | Method callCommand(name, …) | varies | Invoke plugin commands | | Method hasCommand(name) | boolean | Command registered? | | Method getRoot() | HTMLDivElement \| null | .json-tree in shadow DOM | | Event change / json-change | CustomEvent<{ value: string }> | After tree edit (pretty JSON) |

Styles live in an open shadow DOM. Theme tokens sit on :host (see Theming).


DnD (/dnd)

Array reorder is opt-in on Solid so the core entry stays smaller. The web component enables HTML5 DnD by default. Also exports types and path helpers for custom controllers.

import { JsonTreeView } from '@binaryoperations/json-tree-editor';
import { HTML5_ARRAY_REORDER } from '@binaryoperations/json-tree-editor/dnd';

<JsonTreeView
  value={source()}
  onChange={setSource}
  arrayReorder={HTML5_ARRAY_REORDER}
/>

Utils (/utils)

Path helpers (getAtPath, setAtPath, insertAtPath, …), parse helpers (parseJsonSource, JsonValidity), and lower-level primitives. Most apps only need the package root.


Theming

Import exactly one theme. Each file @imports styles.css (structure), so you do not import structure separately. High-contrast replaces default — do not load both.

import '@binaryoperations/json-tree-editor/themes/default.css';
// or:
import '@binaryoperations/json-tree-editor/themes/high-contrast.css';

Theme files set tokens on json-tree-editor and .json-tree. They do not use :host — that selector is the custom element only inside a shadow stylesheet, and this package does not style the host from the shadow. Size json-tree-editor from the page (display: block is in the document copy of styles.css). The web component still inlines structure into its shadow so class rules apply there; the document theme supplies tokens (custom properties inherit).

@import '@binaryoperations/json-tree-editor/themes/default.css';

Force a side on the host (or on .json-tree for Solid):

json-tree-editor,
.json-tree {
  color-scheme: light; /* or dark; omit to keep system */
}

Public tokens use --jte--{block}[--{modifier}|__{element}]. There are no aliases for the old --jte-* names (--jte-bg, --jte-string, …). Value color lives on .json-tree-value--{type} — .json-tree-input--string (and --number / --boolean / --null) are gone.

Token groups

Override any of these on json-tree-editor or .json-tree. Chip -bg / -border tokens are optional; the default preset derives them with color-mix. High-contrast sets opaque fills.

| Group | Tokens | | --- | --- | | --jte--json-tree--* | bg, color, border, muted, surface, font, font-mono, font-size, focus | | Row | --jte--json-tree-row--hover / --focus / --search-active | | Action | --jte--json-tree-action, --jte--json-tree-action--danger, --jte--json-tree-action--disabled | | Key | --jte--json-tree-key, --root, --index | | Type | --jte--json-tree-type--{string\|number\|boolean\|null\|object\|array} (+ optional -bg / -border) | | Value | --jte--json-tree-value--{type} (defaults to the matching type token) | | Mark | --jte--json-tree-mark, --mark-bg, --mark--active, --mark--active-bg | | Flash | --jte--json-tree-flash-ring (fill is color-mix(… 20%, transparent)) | | Error | --jte--json-tree__error, --error-bg, --error-border |

json-tree-editor,
.json-tree {
  --jte--json-tree-type--string: light-dark(#0b3, #6f6);
  --jte--json-tree--font-mono: ui-monospace, Menlo, Consolas, monospace;
  --jte--json-tree--font-size: 12.5px;
  color-scheme: light; /* optional force */
}

Web component ::part map

Stems match BEM after json-tree- / json-tree__. Variants are extra space-separated names on the same node (part="action delete"). There is no disabled part.

| Part | Node | | --- | --- | | tree | .json-tree | | scroll | .json-tree__scroll | | error | .json-tree__error | | search | Find bar | | search-icon | Find icon | | search-input | Find field | | search-count | Match count | | action search-prev / search-next / search-close | Find buttons | | row (+ search-active when that class is on) | Tree row | | drag-handle | Array reorder handle | | chevron (+ open or leaf) | Expand/collapse cue | | key (+ root and/or index) | Property key | | type + JSON type (string, …) | Type chip / select | | value + JSON type | Value wrapper | | input | Key / value input | | null | Null affordance | | summary | Container summary | | actions | Row action cluster | | action duplicate / action delete | Duplicate / delete | | add-row | Expand / add toolbar | | action expand-children / collapse-children | Expand / collapse children | | action add-key / add-item / clear | Add key / item / clear | | children | Nested group | | mark (+ active) | Search highlight | | breadcrumbs | Path bar | | crumb (+ current and/or index) | Path crumb | | breadcrumb-sep | Crumb separator | | breadcrumb-ellipsis | Truncation ellipsis | | flash-ring | Reveal ring (only while .json-tree-flash-ring is on) |

json-tree-editor::part(row) { }
json-tree-editor::part(search) { }
json-tree-editor::part(action delete) { }
json-tree-editor::part(type string) { }
json-tree-editor::part(flash-ring) { }

On the Solid path, the same tokens apply; you can also target BEM classes (.json-tree-row, .json-tree-value--string, …) after importing a theme.


Keyboard

Focus a tree row (or Tab to the active row). Arrow keys move among visible rows. Navigation is disabled while focus is inside an input, select, or textarea.

| Key | Action | | --- | --- | | ↓ / ↑ | Next / previous visible row | | → | Expand collapsed container, else first child | | ← | Collapse expanded container, else parent | | Home / End | First / last visible row | | Cmd/Ctrl+F | Open in-tree find (when search enabled) |

Roving tabindex keeps one visible role="treeitem" tabbable.


Changelog

See CHANGELOG.md.

Demos

Clone the monorepo and run the local demo package (see the repository README):

| Page | Description | | --- | --- | | / | Solid three-pane editor | | /large.html | ~5k-node stress test | | /wc.html | Vanilla web component host | | /history.html | Solid + historyPlugin (stack UI + bootstrap snippet) | | /wc-history.html | WC + historyPlugin (stack UI + bootstrap snippet) |

pnpm install
pnpm dev
# or: pnpm --filter @json-tree-editor/demo dev:history

License

MIT © 2026 Shashank