@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";
const catalog = defineCatalog(schema, {
components: {
...standardComponentDefinitions,
CustomWidget: {
props: z.object({ title: z.string() }),
slots: [],
description: "Custom widget",
},
},
actions: standardActionDefinitions,
});
const { registry } = defineRegistry(catalog, {
components: {
CustomWidget: ({ props }) => <Text>{props.title}</Text>,
} as Components<typeof catalog>,
});
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
Box - Flexbox layout container (like a terminal <div>). Use for grouping, spacing, borders, alignment. Default flexDirection is row.
Text - Text output with optional styling (color, bold, italic, etc.)
Newline - Inserts blank lines. Must be inside a Box with flexDirection column.
Spacer - Flexible empty space that expands along the main axis.
Content
Heading - Section heading (h1: bold+underlined, h2: bold, h3: bold+dimmed, h4: dimmed)
Divider - Horizontal separator with optional centered title
Badge - Colored inline label (variants: default, info, success, warning, error)
Spinner - Animated loading spinner with optional label
ProgressBar - Horizontal progress bar (0-1)
Sparkline - Inline chart using Unicode block characters
BarChart - Horizontal bar chart with labels and values
Table - Tabular data with headers and rows
List - Bulleted or numbered list
ListItem - Structured list row with title, subtitle, leading/trailing text
Card - Bordered container with optional title
KeyValue - Key-value pair display
Link - Clickable URL with optional label
StatusLine - Status message with colored icon (info, success, warning, error)
Markdown - Renders markdown text with terminal styling
Interactive
TextInput - Text input field (events: submit, change)
Select - Selection menu with arrow key navigation (events: change)
MultiSelect - Multi-selection with space to toggle (events: change, submit)
ConfirmInput - Yes/No confirmation prompt (events: confirm, deny)
Tabs - Tab bar navigation with left/right arrow keys (events: change)
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:
{ "$state": "/state/key" } - reads from state model (one-way read)
{ "$bindState": "/path" } - two-way binding: use on the natural value prop of form components
{ "$bindItem": "field" } - two-way binding to a repeat item field
{ "$cond": <condition>, "$then": <value>, "$else": <value> } - conditional value
{ "$template": "Hello, ${/name}!" } - interpolates state values into strings
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>
{}
</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