<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yavuz Özgüven</title>
    <description>The latest articles on DEV Community by Yavuz Özgüven (@yavuzozguven).</description>
    <link>https://dev.to/yavuzozguven</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F802685%2F297cb3d3-2e2a-4e51-a8e8-ab0e3516025e.jpeg</url>
      <title>DEV Community: Yavuz Özgüven</title>
      <link>https://dev.to/yavuzozguven</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yavuzozguven"/>
    <language>en</language>
    <item>
      <title>How I built an interactive JSON visualizer in the browser (no react-flow)</title>
      <dc:creator>Yavuz Özgüven</dc:creator>
      <pubDate>Sat, 16 May 2026 19:15:34 +0000</pubDate>
      <link>https://dev.to/yavuzozguven/how-i-built-an-interactive-json-visualizer-in-the-browser-no-react-flow-a2c</link>
      <guid>https://dev.to/yavuzozguven/how-i-built-an-interactive-json-visualizer-in-the-browser-no-react-flow-a2c</guid>
      <description>&lt;p&gt;Every time I debugged a deeply nested API response, I scrolled. I counted brackets. I lost my place. After the third or fourth time of doing this for the same Stripe webhook, I gave up and built a thing: paste JSON in one side, see it as an interactive graph on the other.&lt;/p&gt;

&lt;p&gt;The result is &lt;a href="https://jsonbloom.com" rel="noopener noreferrer"&gt;jsonbloom.com&lt;/a&gt; — free, runs entirely in the browser, no signup. This post is about the architecture choices behind it, because most of them turned out to be smaller decisions than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not react-flow or d3-hierarchy?
&lt;/h2&gt;

&lt;p&gt;Both are great libraries. I tried both before writing anything custom. The problem is that they're designed for a much broader interaction model than what a JSON viewer actually needs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;react-flow&lt;/strong&gt; is ~150kb gzipped on its own. It supports node dragging, edge editing, mini-maps, custom handles — none of which a JSON visualizer needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;d3-hierarchy&lt;/strong&gt; gives you the layout math, but you still bring your own renderer, your own collapse logic, your own interaction layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What a JSON visualizer actually needs is narrow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Render objects and arrays as boxes&lt;/li&gt;
&lt;li&gt;Draw edges from a parent to each child&lt;/li&gt;
&lt;li&gt;Collapse / expand subtrees&lt;/li&gt;
&lt;li&gt;Pan and zoom the whole canvas&lt;/li&gt;
&lt;li&gt;Edit a leaf value inline&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. No drag-to-rearrange, no node merging, no custom node types. Once I wrote that list down, the case for a custom renderer wrote itself: ~5kb of code that does exactly these five things, versus 150kb of code that does many more things less directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Astro (static shell, SSG)
└── React island (client:only)
    ├── CodeMirror editor on the left
    └── Custom SVG graph on the right
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole landing page — hero, feature cards, FAQ — is plain Astro components rendered at build time. Only the workspace (editor + graph) is a React island, mounted with &lt;code&gt;client:only&lt;/code&gt; because there's no useful SSR for it. This gets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~30kb of JS on the marketing pages (zero React there)&lt;/li&gt;
&lt;li&gt;Fast first paint even on slow connections&lt;/li&gt;
&lt;li&gt;The interactive part loads in parallel while users read the hero&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've used Astro this is the obvious play. If you haven't: it's the single biggest perf win for a "landing page + interactive tool" site, and it's almost free to adopt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The layout
&lt;/h2&gt;

&lt;p&gt;JSON is a tree. The naive thing is to lay it out like an org chart: root at the top, children below. That works for small payloads and breaks immediately for anything realistic — a 50-key object becomes 50 boxes in a row.&lt;/p&gt;

&lt;p&gt;The pattern I settled on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Objects and arrays are boxes&lt;/strong&gt; in a left-to-right tree&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primitive values are pinned next to their key&lt;/strong&gt; inside the parent box, not as separate nodes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each level is its own column&lt;/strong&gt;; siblings stack vertically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This collapses what would be hundreds of nodes into tens. A typical Stripe event renders as maybe 8–15 boxes instead of 80.&lt;/p&gt;

&lt;p&gt;Layout is one pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;COLUMN_WIDTH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;childY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;childY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;childY&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;GAP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NODE_MIN_H&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;childY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No d3, no force simulation. It's deterministic, fast, and you can collapse a subtree by walking the same recursion and skipping its children.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard parts
&lt;/h2&gt;

&lt;p&gt;I underestimated three things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Inline editing that stays in sync.&lt;/strong&gt; The editor on the left is the source of truth. The graph on the right is a view. When you click a value in the graph and edit it, the change has to propagate back to the editor's text — but the editor has cursor position, undo history, syntax highlighting. You can't just &lt;code&gt;setValue&lt;/code&gt; on every edit because you'd nuke the user's cursor every keystroke they made elsewhere.&lt;/p&gt;

&lt;p&gt;The fix was to make the graph send a &lt;em&gt;patch&lt;/em&gt; (JSON pointer + new value) rather than a full document, and have the editor apply patches as small targeted dispatches. CodeMirror's transaction API makes this clean once you stop thinking of the editor as a "text input."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Collapsing without relayout flicker.&lt;/strong&gt; First version: collapse a node → re-run layout for the entire tree → React re-renders everything. The result was a visible jump even on small payloads.&lt;/p&gt;

&lt;p&gt;The fix: layout is incremental. Each subtree owns its own height. When you collapse a node, you only walk &lt;em&gt;up&lt;/em&gt; the tree to fix the y-offsets of its later siblings, not the whole document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Cycles.&lt;/strong&gt; Most people don't realise that JavaScript objects can have cycles but JSON cannot. If you're parsing user input you don't have this problem — &lt;code&gt;JSON.parse&lt;/code&gt; would reject it. But if you ever support &lt;code&gt;eval&lt;/code&gt;-like input (I had this in an early version) you absolutely have to detect cycles before walking, or your renderer locks up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I should have started with the editor → graph protocol (patches) from day one. I started with "the graph re-derives from the parsed JSON every render" and that was fine until it wasn't.&lt;/li&gt;
&lt;li&gt;Hand-rolled SVG was the right call for layout but a slightly less right call for the editing controls. I should have used HTML overlays positioned over the SVG for the inline editors — &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; inside &lt;code&gt;&amp;lt;foreignObject&amp;gt;&lt;/code&gt; has just enough cross-browser jank to be a recurring annoyance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stack summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://astro.build" rel="noopener noreferrer"&gt;Astro&lt;/a&gt; for the shell&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://react.dev" rel="noopener noreferrer"&gt;React&lt;/a&gt; for the workspace island&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://codemirror.net" rel="noopener noreferrer"&gt;CodeMirror 6&lt;/a&gt; for the editor&lt;/li&gt;
&lt;li&gt;Plain SVG + a ~200-line layout function for the graph&lt;/li&gt;
&lt;li&gt;No state management library — everything is local React state plus the JSON document as the single source of truth&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://jsonbloom.com" rel="noopener noreferrer"&gt;jsonbloom.com&lt;/a&gt; — paste any JSON in. I'd especially love feedback on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance with your biggest real-world payloads (a few MB and up)&lt;/li&gt;
&lt;li&gt;Edge cases in the inline editor&lt;/li&gt;
&lt;li&gt;Anything that feels off when you collapse a deeply nested subtree&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's free, no signup, and your JSON never leaves your browser.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>astro</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
