DEV Community

Cover image for I built a copy-paste JSON viewer for shadcn/ui
Marcello Novelli
Marcello Novelli

Posted on

I built a copy-paste JSON viewer for shadcn/ui

Almost every app I work on ends up showing raw JSON somewhere: API responses, webhook payloads, debug panels. A <pre>{JSON.stringify(data, null, 2)}</pre> gets hard to read fast, and most JSON viewer packages bring their own styles and never quite match a shadcn/ui app.

At the same time, when reading raw data, logs, analytics events, etc., having a decent way to quickly visualise complex json payloads is quite important (or at least it is for me!)

So I built json-view-cn: a collapsible, syntax-highlighted JSON tree viewer you install the shadcn way. It's one file you own, styled with Tailwind.

👉 Live demo

GitHub logo mnove / json-view-cn

Fully-customizable, collapsible, syntax-highlighted JSON tree viewer. Ready to be copy-pasted.

json-view-cn

json-view-cn

A fully-customizable, collapsible, syntax-highlighted JSON tree viewer component for React. Built with shadcn/ui, Base UI, and Tailwind CSS v4. Ready to be copy-pasted into your project.

Live demo

Installation

npx shadcn@latest add https://json-view-cn.vercel.app/r/json-view.json
Enter fullscreen mode Exit fullscreen mode

Or, if the @json-view-cn namespace is configured (see Registry):

npx shadcn@latest add @json-view-cn/json-view
Enter fullscreen mode Exit fullscreen mode

The component is added to your ui alias (e.g. components/ui/json-view.tsx).

Note: The component targets shadcn/ui projects that use Base UI primitives (e.g. the base-mira style). It relies on the Base UI render prop of TooltipTrigger, so projects using the Radix-based shadcn/ui components will need to adapt the tooltip usage (e.g. to asChild).

Dependencies

The component relies on these shadcn/ui components:

  • button
  • tooltip

And the following packages:

  • lucide-react
  • clsx / tailwind-merge (via the cn utility)

Usage

import { JsonView } from "@/components/ui/json-view"
const data = {
  name: "John Doe",
  age: 32,
  isActive: 
…
Enter fullscreen mode Exit fullscreen mode

Getting started

npx shadcn@latest add https://json-view-cn.vercel.app/r/json-view.json
Enter fullscreen mode Exit fullscreen mode
import { JsonView } from "@/components/ui/json-view"

<JsonView data={response} rootName="response" initialDepth={1} stringTruncate={60} />
Enter fullscreen mode Exit fullscreen mode

Features

  • Collapsible nodes with …3 keys / …12 items summaries
  • Syntax highlighting that works in light and dark mode, themeable with plain Tailwind classes
  • Copy on hover for any line (objects are copied as pretty-printed JSON)
  • Indent guides, string truncation with tooltip preview, and depth control
  • Keyboard accessible, with real buttons, focus rings and aria-expanded
  • No extra dependencies beyond shadcn's button and tooltip and lucide-react

Theming is just a map of class names:

<JsonView
  data={data}
  theme={{ key: "text-rose-500 dark:text-rose-400", string: "text-yellow-600 dark:text-yellow-300" }}
/>
Enter fullscreen mode Exit fullscreen mode

The tricky parts (and what I learnt)

Real data isn't always JSON

In real app state you'll find Dates, Maps, bigint IDs and the odd stray function. So data is typed as unknown, and values are rendered as close as possible to what JSON.stringify would produce. It respects toJSON() (so a Date shows its ISO string), turns Map into an object and Set into an array, and shows bigint, undefined, functions and symbols without quotes so they can't be mistaken for strings. It never throws.

Circular is not the same as "seen before"

The obvious fix for circular references is to keep a set of every object you've visited. But that's wrong:

const address = { city: "Springfield" }
const data = { billing: address, shipping: address }
Enter fullscreen mode Exit fullscreen mode

address appears twice, but nothing here is circular. A reference is only circular if it points back to one of its own ancestors. So each node passes its ancestor chain down to its children instead. The copy button does the same thing inside a JSON.stringify replacer, using the fact that this is the parent object of the value being serialized.

Escaping in one line

Strings with quotes, backslashes or newlines need to display the way they would in a .json file. The simplest way to do that is to let JSON escape them:

const escapeString = (value: string) => JSON.stringify(value).slice(1, -1)
Enter fullscreen mode Exit fullscreen mode

These are just a few things of the bugs/edge cases I encountered when developing this.

Shipped as a shadcn registry

The repo is a shadcn registry. shadcn build generates static JSON that's served from the demo site, and the CLI installs button and tooltip for you if they're missing. You can also add the @json-view-cn namespace to components.json and run npx shadcn@latest add @json-view-cn/json-view.

Note: it targets shadcn/ui projects that use Base UI primitives. On the Radix-based components, swap the tooltip's render prop for asChild. That's a two-line change in a file you own. I might also add a Radix and React Aria version of this in the near future...

Try it

It's MIT licensed. If you find data that renders wrong, open an issue with the payload. Thanks for reading! 🙌

Top comments (0)