Json-render ink
Ink terminal renderer for json-render that turns JSON specs into interactive terminal UIs. Use when working with @json-render/ink, building terminal UIs from JSON, creating terminal component catalogs, or rendering AI-generated specs in the terminal.
install
source · Clone the upstream repo
git clone https://github.com/vercel-labs/json-render
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/vercel-labs/json-render "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/ink" ~/.claude/skills/vercel-labs-json-render-ink && rm -rf "$T"
manifest:
skills/ink/SKILL.mdsource content
@json-render/ink
Ink terminal renderer that converts JSON specs into interactive terminal component trees with standard components, data binding, visibility, actions, and dynamic props.
Quick Start
import { defineCatalog } from "@json-render/core"; import { schema } from "@json-render/ink/schema"; import { standardComponentDefinitions, standardActionDefinitions, } from "@json-render/ink/catalog"; import { defineRegistry, Renderer, type Components } from "@json-render/ink"; import { z } from "zod"; // Create catalog with standard + custom components const catalog = defineCatalog(schema, { components: { ...standardComponentDefinitions, CustomWidget: { props: z.object({ title: z.string() }), slots: [], description: "Custom widget", }, }, actions: standardActionDefinitions, }); // Register only custom components (standard ones are built-in) const { registry } = defineRegistry(catalog, { components: { CustomWidget: ({ props }) => <Text>{props.title}</Text>, } as Components<typeof catalog>, }); // Render function App({ spec }) { return ( <JSONUIProvider initialState={{}}> <Renderer spec={spec} registry={registry} /> </JSONUIProvider> ); }
Spec Structure (Flat Element Map)
The Ink schema uses a flat element map with a root key:
{ "root": "main", "elements": { "main": { "type": "Box", "props": { "flexDirection": "column", "padding": 1 }, "children": ["heading", "content"] }, "heading": { "type": "Heading", "props": { "text": "Dashboard", "level": "h1" }, "children": [] }, "content": { "type": "Text", "props": { "text": "Hello from the terminal!" }, "children": [] } } }
Standard Components
Layout
- Flexbox layout container (like a terminalBox
). Use for grouping, spacing, borders, alignment. Default flexDirection is row.<div>
- Text output with optional styling (color, bold, italic, etc.)Text
- Inserts blank lines. Must be inside a Box with flexDirection column.Newline
- Flexible empty space that expands along the main axis.Spacer
Content
- Section heading (h1: bold+underlined, h2: bold, h3: bold+dimmed, h4: dimmed)Heading
- Horizontal separator with optional centered titleDivider
- Colored inline label (variants: default, info, success, warning, error)Badge
- Animated loading spinner with optional labelSpinner
- Horizontal progress bar (0-1)ProgressBar
- Inline chart using Unicode block charactersSparkline
- Horizontal bar chart with labels and valuesBarChart
- Tabular data with headers and rowsTable
- Bulleted or numbered listList
- Structured list row with title, subtitle, leading/trailing textListItem
- Bordered container with optional titleCard
- Key-value pair displayKeyValue
- Clickable URL with optional labelLink
- Status message with colored icon (info, success, warning, error)StatusLine
- Renders markdown text with terminal stylingMarkdown
Interactive
- Text input field (events: submit, change)TextInput
- Selection menu with arrow key navigation (events: change)Select
- Multi-selection with space to toggle (events: change, submit)MultiSelect
- Yes/No confirmation prompt (events: confirm, deny)ConfirmInput
- Tab bar navigation with left/right arrow keys (events: change)Tabs
Visibility Conditions
Use
visible on elements to show/hide based on state. Syntax: { "$state": "/path" }, { "$state": "/path", "eq": value }, { "$state": "/path", "not": true }, { "$and": [cond1, cond2] } for AND, { "$or": [cond1, cond2] } for OR.
Dynamic Prop Expressions
Any prop value can be a data-driven expression resolved at render time:
- reads from state model (one-way read){ "$state": "/state/key" }
- two-way binding: use on the natural value prop of form components{ "$bindState": "/path" }
- two-way binding to a repeat item field{ "$bindItem": "field" }
- conditional value{ "$cond": <condition>, "$then": <value>, "$else": <value> }
- interpolates state values into strings{ "$template": "Hello, ${/name}!" }
Components do not use a
statePath prop for two-way binding. Use { "$bindState": "/path" } on the natural value prop instead.
Event System
Components use
emit to fire named events. The element's on field maps events to action bindings:
CustomButton: ({ props, emit }) => ( <Box> <Text>{props.label}</Text> {/* emit("press") triggers the action bound in the spec's on.press */} </Box> ),
{ "type": "CustomButton", "props": { "label": "Submit" }, "on": { "press": { "action": "submit" } }, "children": [] }
Built-in Actions
setState, pushState, and removeState are built-in and handled automatically:
{ "action": "setState", "params": { "statePath": "/activeTab", "value": "home" } } { "action": "pushState", "params": { "statePath": "/items", "value": { "text": "New" } } } { "action": "removeState", "params": { "statePath": "/items", "index": 0 } }
Repeat (Dynamic Lists)
Use the
repeat field on a container element to render items from a state array:
{ "type": "Box", "props": { "flexDirection": "column" }, "repeat": { "statePath": "/items", "key": "id" }, "children": ["item-row"] }
Inside repeated children, use
{ "$item": "field" } to read from the current item and { "$index": true } for the current index.
Streaming
Use
useUIStream to progressively render specs from JSONL patch streams:
import { useUIStream } from "@json-render/ink"; const { spec, send, isStreaming } = useUIStream({ api: "/api/generate" });
Server-Side Prompt Generation
Use the
./server export to generate AI system prompts from your catalog:
import { catalog } from "./catalog"; const systemPrompt = catalog.prompt({ system: "You are a terminal assistant." });
Providers
| Provider | Purpose |
|---|---|
| Share state across components (JSON Pointer paths). Accepts optional prop for controlled mode. |
| Handle actions dispatched via the event system |
| Enable conditional rendering based on state |
| Form field validation |
| Manage focus across interactive components |
| Combined provider for all contexts |
External Store (Controlled Mode)
Pass a
StateStore to StateProvider (or JSONUIProvider) to use external state management:
import { createStateStore, type StateStore } from "@json-render/ink"; const store = createStateStore({ count: 0 }); <StateProvider store={store}>{children}</StateProvider> store.set("/count", 1); // React re-renders automatically
When
store is provided, initialState and onStateChange are ignored.
createRenderer (Higher-Level API)
import { createRenderer } from "@json-render/ink"; import { standardComponents } from "@json-render/ink"; import { catalog } from "./catalog"; const InkRenderer = createRenderer(catalog, { ...standardComponents, // custom component overrides here }); // InkRenderer includes all providers (state, visibility, actions, focus) render( <InkRenderer spec={spec} state={{ activeTab: "overview" }} /> );
Key Exports
| Export | Purpose |
|---|---|
| Create a type-safe component registry from a catalog |
| Render a spec using a registry |
| Higher-level: creates a component with built-in providers |
| Combined provider for all contexts |
| Ink flat element map schema (includes built-in state actions) |
| Catalog definitions for all standard components |
| Catalog definitions for standard actions |
| Pre-built component implementations |
| Access state context |
| Get single value from state |
| Two-way binding for / expressions |
| Access actions context |
| Get a single action dispatch function |
| Non-throwing variant of useValidation |
| Stream specs from an API endpoint |
| Create a framework-agnostic in-memory |
| Interface for plugging in external state management |
| Typed component map (catalog-aware) |
| Typed action map (catalog-aware) |
| Typed component context (catalog-aware) |
| Convert flat element map to tree structure |
Terminal UI Design Guidelines
- Use Box for layout (flexDirection, padding, gap). Default flexDirection is row.
- Terminal width is ~80-120 columns. Prefer vertical layouts (flexDirection: column) for main structure.
- Use borderStyle on Box for visual grouping (single, double, round, bold).
- Use named terminal colors: red, green, yellow, blue, magenta, cyan, white, gray.
- Use Heading for section titles, Divider to separate sections, Badge for status, KeyValue for labeled data, Card for bordered groups.
- Use Tabs for multi-view UIs with visible conditions on child content.
- Use Sparkline for inline trends and BarChart for comparing values.