ink
Original:🇺🇸 English
Translated
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.
3installs
Sourcevercel-labs/json-render
Added on
NPX Install
npx skill4agent add vercel-labs/json-render inkTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →@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
typescript
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:
json
{
"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 terminal
Box). 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 title
Divider - - Colored inline label (variants: default, info, success, warning, error)
Badge - - Animated loading spinner with optional label
Spinner - - Horizontal progress bar (0-1)
ProgressBar - - Inline chart using Unicode block characters
Sparkline - - Horizontal bar chart with labels and values
BarChart - - Tabular data with headers and rows
Table - - Bulleted or numbered list
List - - Structured list row with title, subtitle, leading/trailing text
ListItem - - Bordered container with optional title
Card - - Key-value pair display
KeyValue - - Clickable URL with optional label
Link - - Status message with colored icon (info, success, warning, error)
StatusLine - - Renders markdown text with terminal styling
Markdown
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 on elements to show/hide based on state. Syntax: , , , for AND, for OR.
visible{ "$state": "/path" }{ "$state": "/path", "eq": value }{ "$state": "/path", "not": true }{ "$and": [cond1, cond2] }{ "$or": [cond1, cond2] }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 prop for two-way binding. Use on the natural value prop instead.
statePath{ "$bindState": "/path" }Event System
Components use to fire named events. The element's field maps events to action bindings:
emitontsx
CustomButton: ({ props, emit }) => (
<Box>
<Text>{props.label}</Text>
{/* emit("press") triggers the action bound in the spec's on.press */}
</Box>
),json
{
"type": "CustomButton",
"props": { "label": "Submit" },
"on": { "press": { "action": "submit" } },
"children": []
}Built-in Actions
setStatepushStateremoveStatejson
{ "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 field on a container element to render items from a state array:
repeatjson
{
"type": "Box",
"props": { "flexDirection": "column" },
"repeat": { "statePath": "/items", "key": "id" },
"children": ["item-row"]
}Inside repeated children, use to read from the current item and for the current index.
{ "$item": "field" }{ "$index": true }Streaming
Use to progressively render specs from JSONL patch streams:
useUIStreamtsx
import { useUIStream } from "@json-render/ink";
const { spec, send, isStreaming } = useUIStream({ api: "/api/generate" });Server-Side Prompt Generation
Use the export to generate AI system prompts from your catalog:
./servertypescript
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 |
| 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 to (or ) to use external state management:
StateStoreStateProviderJSONUIProvidertsx
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 automaticallyWhen is provided, and are ignored.
storeinitialStateonStateChangecreateRenderer (Higher-Level API)
tsx
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 |
| 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.