/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.
$ npx -y skills add vercel-labs/json-render --skill ink --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/ink
Context preview
The summary Claude sees to decide when to auto-load this skill.
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.
SKILL.md
ink.SKILL.mdname: ink
description: 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.
@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
- `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>
{/* 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 "Read more
name: ink description: 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.
@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
- `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>
{/* 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 "The Generative UI framework. Generate dynamic, personalized UIs from prompts without sacrificing reliability. Predefined components and actions for safe, predictable output.
Repo: vercel-labs/json-render
Other skills on json-render.
- /codegen
Code generation utilities for json-render. Use when generating code from UI specs, building custom code exporters, traversing specs, or serializing props for @json-render/codegen.
Open skill - /core
Core package for defining schemas, catalogs, and AI prompt generation for json-render. Use when working with @json-render/core, defining schemas, creating catalogs, or building JSON specs for UI/video generation.
Open skill - /devtools
Drop-in inspector panel for any json-render app. Use when the user wants to debug a generative UI, inspect the spec tree, edit state at runtime, see dispatched actions, follow stream patches live, browse a catalog, or pick DOM elements to find their spec keys. Triggers include
Open skill - /directives
Pre-built custom directives for json-render — formatting, math, string manipulation, and i18n. Use when working with @json-render/directives, defining custom directives with defineDirective, or adding $format, $math, $concat, $count, $truncate, $pluralize, $join, or $t to specs.
Open skill - /image
Image renderer for json-render that turns JSON specs into SVG and PNG images via Satori. Use when working with @json-render/image, generating OG images from JSON, creating social cards, or rendering AI-generated image specs.
Open skill - /jotai
Jotai adapter for json-render's StateStore interface. Use when integrating json-render with Jotai for state management via @json-render/jotai.
Open skill

