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

react-json-virtualization

v0.7.0

Published

Virtualized React JSON viewer for very large JSON strings with token-based color theming.

Readme

react-json-virtualization

Virtualized React JSON viewer for large JSON strings with token-based color theming.

Features

  • Incremental async JSON parsing on the main thread
  • Virtualized tree rendering for large nested structures
  • JSON-only metadata/tree mode
  • Plain virtualized text mode for non-JSON text-like formats (for example Markdown, XML, YAML, TXT)
  • Expand/collapse nodes with keyboard support
  • Path and primitive-value search over JSON paths
  • Dedicated searchQuery support with AND-combination against pathFilterQuery
  • Direct-match highlighting for tree rows and pretty-printed lines
  • Active match navigation via activeMatchIndex for external search UI
  • Row-level customization hooks (filter, decorators, render overrides)
  • Trie-indexed prefix path search for low-latency filtering on large trees
  • Controlled and uncontrolled expansion state
  • Token-based color theme overrides (keys, values, punctuation)
  • TypeScript-first public API

Install

npm install react-json-virtualization

Demo

Try the live demo:

https://mietoprogramming.github.io/react-json-virtualization/

Mode comparison note (Collapsable vs Static):

demo/public/docs/virtualizejson-modes.md

Benchmark Snapshot (Modes)

Measured with:

npm run bench:modes

Environment: Node v24.14.1, linux x64, fixtures from bench/fixtures/generated, 5 iterations.

Scope: Collapsable and Static include parse + expansion + flatten work. Plain (metadata=false) measures pretty-line generation only.

large-10mb.json (10.00 MB)

| Mode | Avg (ms) | Min (ms) | Max (ms) | Output size | vs Collapsable | | --- | ---: | ---: | ---: | ---: | ---: | | Collapsable (metadata=true, depth=1) | 1024.77 | 1017.08 | 1042.66 | 64,838 rows | 1.00x | | Static (metadata=true, alwaysExpanded) | 1384.20 | 1308.87 | 1468.24 | 842,834 rows | 1.35x | | Plain (metadata=false) | 136.72 | 127.33 | 143.63 | 1,037,335 lines | 0.13x |

large-50mb.json (50.00 MB)

| Mode | Avg (ms) | Min (ms) | Max (ms) | Output size | vs Collapsable | | --- | ---: | ---: | ---: | ---: | ---: | | Collapsable (metadata=true, depth=1) | 5230.99 | 4973.87 | 5534.76 | 324,181 rows | 1.00x | | Static (metadata=true, alwaysExpanded) | 7630.65 | 7380.08 | 8191.26 | 4,214,293 rows | 1.46x | | Plain (metadata=false) | 911.44 | 839.57 | 1030.96 | 5,186,823 lines | 0.17x |

large-100mb.json (100.00 MB)

| Mode | Avg (ms) | Min (ms) | Max (ms) | Output size | vs Collapsable | | --- | ---: | ---: | ---: | ---: | ---: | | Collapsable (metadata=true, depth=1) | 10771.17 | 10303.29 | 11218.33 | 648,359 rows | 1.00x | | Static (metadata=true, alwaysExpanded) | 16062.59 | 15595.29 | 16422.84 | 8,428,607 rows | 1.49x | | Plain (metadata=false) | 1776.98 | 1639.63 | 1935.16 | 10,373,671 lines | 0.16x |

Plain is fastest in this end-to-end benchmark because metadata=false bypasses incremental tree parsing and flattening.

Usage

import { VirtualizeJSON } from "react-json-virtualization";

const payload = JSON.stringify({
  users: [{ id: 1, name: "Ada" }, { id: 2, name: "Linus" }],
  stats: { total: 2, active: true }
});

export function Example() {
  return (
    <VirtualizeJSON.Collapsable
      json={payload}
      metadata={true}
      height={480}
      rowHeight={24}
      initialExpandDepth={1}
      pathFilterQuery="$.users[1]"
      theme={{
        key: "#8a4f00",
        string: "#006b4f",
        number: "#094b9d"
      }}
    />
  );
}

Static, always-expanded viewer:

import { VirtualizeJSON } from "react-json-virtualization";

export function StaticExample({ json }: { json: string }) {
  return <VirtualizeJSON.Static json={json} height={480} rowHeight={24} />;
}

Search navigation (Ctrl+F-style UI managed by the host app):

import { useState } from "react";
import { VirtualizeJSON, type JSONViewerSearchMetadata } from "react-json-virtualization";

export function SearchableViewer({ json }: { json: string }) {
  const [searchQuery, setSearchQuery] = useState("");
  const [activeMatchIndex, setActiveMatchIndex] = useState<number | null>(null);
  const [searchMetadata, setSearchMetadata] = useState<JSONViewerSearchMetadata | null>(null);

  const availableMatches = searchMetadata?.matchedRowIds.length ?? 0;
  const hasMore = searchMetadata?.hasMore ?? false;

  const goNext = (): void => {
    if (availableMatches === 0) {
      return;
    }

    setActiveMatchIndex((current) => {
      const next = current === null ? 0 : (current + 1) % availableMatches;
      return next;
    });
  };

  const goPrev = (): void => {
    if (availableMatches === 0) {
      return;
    }

    setActiveMatchIndex((current) => {
      const next = current === null ? availableMatches - 1 : (current - 1 + availableMatches) % availableMatches;
      return next;
    });
  };

  return (
    <div>
      <div>
        <input value={searchQuery} onChange={(event) => setSearchQuery(event.target.value)} />
        <button type="button" onClick={goPrev}>Prev</button>
        <button type="button" onClick={goNext}>Next</button>
        <span>
          {availableMatches === 0 ? "0" : `${(activeMatchIndex ?? 0) + 1} / ${availableMatches}${hasMore ? "+" : ""}`}
        </span>
      </div>
      <VirtualizeJSON.Collapsable
        json={json}
        searchQuery={searchQuery}
        activeMatchIndex={activeMatchIndex}
        onSearchMetadata={setSearchMetadata}
      />
    </div>
  );
}

Row customization (highlight, actions, and custom rendering):

import { VirtualizeJSON, type JSONViewerRowDecorator, type JSONViewerRowFilter, type JSONViewerRowRenderer } from "react-json-virtualization";

const rowFilter: JSONViewerRowFilter = (context) => {
  if (context.text.toLowerCase().includes("internal")) {
    return false;
  }

  return true;
};

const rowDecorator: JSONViewerRowDecorator = (context) => {
  if (!context.text.toLowerCase().includes("craw")) {
    return undefined;
  }

  return {
    className: "row-highlight",
    actions: (
      <button type="button" onClick={(event) => event.stopPropagation()}>
        Flag
      </button>
    )
  };
};

const rowRenderer: JSONViewerRowRenderer = (context, defaultContent) => {
  if (!context.text.toLowerCase().includes("craw")) {
    return defaultContent;
  }

  return (
    <button type="button" onClick={(event) => event.stopPropagation()}>
      {defaultContent}
    </button>
  );
};

export function CustomRows({ json }: { json: string }) {
  return (
    <VirtualizeJSON.Collapsable
      json={json}
      rowFilter={rowFilter}
      rowDecorator={rowDecorator}
      rowRenderer={rowRenderer}
    />
  );
}

Note: custom render content should stay within the fixed row height to keep virtualization accurate.

API

VirtualizeJSON.Collapsable props

  • json: string Raw JSON string input.
  • sourceFormat?: "auto" | "json" | "yaml" | "xml" | "markdown" | "text" Input format hint. Default "auto", which infers by content. All non-JSON formats render in plain virtualized mode.
  • metadata?: boolean Enables tree metadata mode (Object/Array counts, expansion, filtering, virtualization). Default true. Tree metadata is available only for json. All non-JSON formats render a virtualized plain-text view.
  • showLineNumbers?: boolean Shows line numbers in the virtualized pretty-printed JSON view (metadata=false). Default true.
  • height?: number | string Container height. Default 520.
  • rowHeight?: number Fixed row height used by virtualization. Default 24.
  • overscan?: number Number of extra rows rendered around viewport. Default 8.
  • initialExpandDepth?: number Initial expansion depth from root. Default 1.
  • expandedPaths?: ReadonlySet<string> Controlled expanded path set.
  • defaultExpandedPaths?: Iterable<string> Initial expanded paths in uncontrolled mode.
  • onExpandedPathsChange?: (paths) => void Expansion state callback.
  • pathFilterQuery?: string Filters by JSON path and all JSON value types (string, number, boolean, null, object, array). Unquoted terms are split by whitespace and matched with OR semantics (zero hello matches either term). Wrap exact phrases in quotes (for example "new york" name). Quoted single terms are treated the same as unquoted terms. In metadata=false, it filters pretty-printed lines.
  • searchQuery?: string Highlights matches without filtering rows or lines. Uses the same tokenization as pathFilterQuery. Match strategy is controlled by searchMode (defaults to pathFilterMode); case sensitivity is controlled by searchCaseSensitive.
  • activeMatchIndex?: number | null 0-based index into the current matchedPaths (tree) or matchedLineNumbers (plain) list from onSearchMetadata. The viewer scrolls to the active match and applies an active-match highlight. In tree mode, the viewer also updates internal selection when selectedPath is uncontrolled.
  • pathFilterCaseSensitive?: boolean Case-sensitive path filter mode.
  • searchCaseSensitive?: boolean Case-sensitive search mode (applies to path and value matching). Default false. Note: prior versions inherited pathFilterCaseSensitive for search; set this to true to preserve that behavior.
  • pathFilterMode?: "auto" | "prefix" | "includes" | "exact" Filter strategy. Defaults to auto. exact matches full path/segments and full values.
  • searchMode?: "auto" | "prefix" | "includes" | "exact" Search strategy. Defaults to pathFilterMode. exact matches full path/segments and full values.
  • searchMetadataLimit?: number Maximum identifiers returned in search metadata arrays. Default 500.
  • theme?: JsonThemeOverride Per-token color overrides.
  • selectedPath?: string Controlled selected node path.
  • className?: string Optional custom class.
  • rowFilter?: (context) => boolean Optional row/line filter applied after pathFilterQuery and before search metadata. Return true to keep a row, false to hide it.
  • rowDecorator?: (context) => { className?; style?; leading?; trailing?; actions? } Optional per-row decoration for classes, styles, and extra UI slots.
  • rowRenderer?: (context, defaultContent) => ReactNode Optional override for the row body content while keeping the row container intact.
  • onNodeClick?: (path, row) => void Node selection callback.
  • onSearchMetadata?: (metadata) => void Search callback with counts and capped match identifiers for both metadata=true (tree rows) and metadata=false (pretty lines). Does not filter the view.
  • onParseProgress?: (processed, total) => void Parse progress callback.
  • onParseError?: (error) => void Parse error callback.

onSearchMetadata payload fields:

  • mode: "tree" | "plain"
  • query, pathFilterQuery, searchQuery
  • matchCount (direct matches)
  • visibleCount (rows or lines currently visible after context inclusion)
  • matchedPaths, matchedRowIds, matchedLineNumbers (all capped by searchMetadataLimit)
  • hasMore (true when any metadata list was truncated)

When hasMore is true, matchCount can exceed the length of matchedPaths or matchedLineNumbers. Use searchMetadataLimit to raise the cap if you need to navigate every match.

Row customization context fields (both modes) include:

  • mode: tree or plain
  • id: row id (row.id) or line id (line:${lineNumber})
  • text: row or line text used for quick matching
  • sourceFormat: resolved source format

Tree mode adds: path, row (the full FlatJsonRow).

Plain mode adds: line, lineIndex, lineNumber.

VirtualizeJSON.Static props

All VirtualizeJSON.Collapsable props except expansion control props:

  • Omitted: initialExpandDepth
  • Omitted: expandedPaths
  • Omitted: defaultExpandedPaths
  • Omitted: onExpandedPathsChange

VirtualizeJSON.Static always renders all expandable paths and disables collapse/expand interactions.

Migration (breaking change)

  • JSONViewer was renamed to VirtualizeJSON.Collapsable.
  • Static always-expanded mode is available as VirtualizeJSON.Static.
  • pathFilterQuery now treats unquoted whitespace as multiple OR terms. Use quotes to keep multi-word phrases together.

Expansion helper exports

  • createExpandedPathSet(paths?)
  • expandPath(current, path)
  • collapsePath(current, path)
  • toggleExpandedPath(current, path)
  • expandedPathsFromDepth(root, depth)

Path filter helper export

  • filterRowsByPathQuery(rows, query, options?)
  • createPathSearchIndex(rows, { caseSensitive? })

For best path-filter performance on very large expanded row sets, use mode: "prefix" with a cached index from createPathSearchIndex.