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…

INSTALLATION
npx skills add https://github.com/vercel-labs/json-render --skill ink
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

// 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 }) => {props.title},

} as Components,

});

// Render

function App({ spec }) {

return (

);

}

## 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 "@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

`StateProvider`
Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode.

`ActionProvider`
Handle actions dispatched via the event system

`VisibilityProvider`
Enable conditional rendering based on state

`ValidationProvider`
Form field validation

`FocusProvider`
Manage focus across interactive components

`JSONUIProvider`
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" }} />

);

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card