A collapsible JSON inspector with compact type styling and keyboard-operable branches.
import { JsonViewer } from "@/components/ballmac/json-viewer"
const response = {
workspace: "Product design",
active: true,
members: 12,
features: { analytics: true, exports: ["csv", "pdf"] },
updatedAt: null,
}
export default function JsonViewerDemo() {
return (
<JsonViewer
className="w-full max-w-sm"
value={response}
label="Workspace API response"
defaultExpandedDepth={2}
/>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/json-viewerInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/json-viewer.tsx// Ballmac UI: JSON Viewer. https://ui.ballmac.com/components/json-viewer "use client" import * as React from "react" import { ChevronRight } from "lucide-react" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" type JsonViewerProps = Omit<React.ComponentProps<"div">, "children"> & { /** JSON-compatible value to inspect. */ value: unknown /** Accessible label for the data. */ label?: string /** Number of levels open initially. */ defaultExpandedDepth?: number } function JsonViewer({ className, value, label, defaultExpandedDepth = 1, ...props }: JsonViewerProps) { const msg = useMessages() label ??= msg("json-viewer.label", "JSON data") const [overrides, setOverrides] = React.useState<Record<string, boolean>>({}) function renderNode( name: string | undefined, item: unknown, path: string, depth: number, ): React.ReactNode { const nested = item !== null && typeof item === "object" const entries = nested ? Object.entries(item as Record<string, unknown>) : [] const expanded = overrides[path] ?? depth < defaultExpandedDepth const summary = Array.isArray(item) ? `[${entries.length}]` : `{${entries.length}}` return ( <div key={path} data-slot="json-viewer-node" className="min-w-0"> <div className="flex min-w-0 items-start gap-1 leading-6" style={{ paddingInlineStart: `${depth * 16}px` }} > {nested ? ( <button type="button" data-slot="json-viewer-toggle" aria-label={expanded ? msg("json-viewer.collapse", "Collapse {name}", { name: name ?? msg("json-viewer.root", "root") }) : msg("json-viewer.expand", "Expand {name}", { name: name ?? msg("json-viewer.root", "root") })} aria-expanded={expanded} onClick={() => setOverrides((current) => ({ ...current, [path]: !expanded })) } className="hover:bg-accent mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" > <ChevronRight aria-hidden="true" className={cn( "size-3.5 transition-transform duration-150 motion-reduce:transition-none", expanded ? "rotate-90" : "rtl:rotate-180", )} /> </button> ) : ( <span aria-hidden="true" className="size-5 shrink-0" /> )} <span className="min-w-0 break-all"> {name !== undefined && ( <span className="text-muted-foreground"> {JSON.stringify(name)}:{" "} </span> )} {nested ? ( <span className="text-muted-foreground">{summary}</span> ) : ( <span className={cn( typeof item === "string" ? "text-foreground" : typeof item === "number" || typeof item === "boolean" ? "text-primary" : "text-muted-foreground", )} > {JSON.stringify(item) ?? "undefined"} </span> )} </span> </div> {nested && expanded && ( <div data-slot="json-viewer-children"> {entries.map(([key, child]) => renderNode( key, child, `${path}.${JSON.stringify(key)}`, depth + 1, ), )} </div> )} </div> ) } return ( <div data-slot="json-viewer" dir="ltr" role="region" aria-label={label} tabIndex={0} className={cn( "bg-card max-h-72 min-w-0 max-w-full overflow-auto rounded-xl border border-border p-3 font-mono text-xs outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50", className, )} {...props} > {renderNode(undefined, value, "$", 0)} </div> ) } export { JsonViewer, type JsonViewerProps }Update the import paths to match your project setup.
Usage
import { JsonViewer } from "@/components/ballmac/json-viewer"The full example is in the Code tab above.
Examples
States and variants
import { JsonViewer } from "@/components/ballmac/json-viewer"
export default function JsonViewerStates() {
return (
<JsonViewer
className="w-full max-w-sm"
value={[
{ id: "evt_001", status: "complete" },
{ id: "evt_002", status: "pending" },
]}
label="Recent events JSON"
defaultExpandedDepth={1}
/>
)
}API reference
| Prop | Type | Default |
|---|---|---|
value*JSON-compatible value to inspect. | unknown | — |
labelAccessible label for the data. | string | — |
defaultExpandedDepthNumber of levels open initially. | number | 1 |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab / Enter / Space | Focuses and toggles JSON branches |
Use with AI
A collapsible JSON inspector with compact type styling and keyboard-operable branches. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI JSON Viewer (@ballmac/json-viewer) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Inspect nested API responses or configuration
Not for
- Use Code Block for unstructured text
Registry JSON: https://ui.ballmac.com/r/json-viewer.json
Credits
Free to use in personal and commercial projects.
- npm
- lucide-react
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Code Block
A code panel with filename and language header, line numbers, highlighted lines, a wrap toggle, copy feedback and file tabs. No highlighter bundled; pass Shiki output as children.
Activity Feed
A compact actor and action feed with timestamps and an empty state.
Audio Player
A podcast-style audio player: play and pause, skip back and forward, a scrubber with chapter ticks, a named current chapter and a speed control. Works with a real file or as a silent demo.
Avatar Circles
Overlapping avatars that spread on hover, lift individually with a name and role tooltip, show presence, fall back to initials and end in a +N count that can be a button.