Skip to content
Frontend
Skill

/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.

From plugin
json-render
19k31 skills

Claude Code

$ npx -y skills add vercel-labs/json-render --skill ink --agent claude-code

SKILL.md

ink.SKILL.md
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.

For nested lists, an inner repeat can use `{ "statePath": { "$item": "children" } }` to iterate an array on the enclosing ite

Read more
@vercel-labs-2
Ships withjson-render

The Generative UI framework. Generate dynamic, personalized UIs from prompts without sacrificing reliability. Predefined components and actions for safe, predictable output.

Stars on GitHub

Momentum

stars in total
18.9k
Star history of vercel-labs/json-render on GitHub, a chart by star-history.com

Other skills on json-render.

codegen

codegen

Code generation utilities for json-render. Use when generating code from UI specs, building custom code exporters, traversing…

core

core

Core package for defining schemas, catalogs, and AI prompt generation for json-render. Use when working with @json-render/core,…

More skills from other plugins.

next-bundle-optimizer
Auto-invoked

next-bundle-optimizer

Audit and reduce Next.js browser initial-load work. Use for slow route startup, oversized client bundles, duplicate browser…

nextjs2d143k
@vercel@vercelInstall Skill
migrate-radix-to-base

migrate-radix-to-base

Migrates React projects and components from Radix UI to Base UI. Use when asked to migrate from radix, move to base-ui, convert…

ui11d127k
@shadcn-ui@shadcn-uiInstall Skill
chrome-release-cls

chrome-release-cls

Given a Chrome Releases blog post URL (chromereleases.googleblog.com), extract every CVE/bug and find the underlying Gerrit CL…

electron2d124k
@electron@electronInstall Skill
taste-skill
Auto-invoked

taste-skill

tasteskill v2, the anti-slop frontend skill. Art direction for websites, distilled from a study of 247 curated gallery sites (One…

@leonxlnx@leonxlnxInstall Skill

Questions about ink.

Which coding agents can use ink?
ink installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.