Skip to content

Silvery vs Ink

External project claims last verified: 2026-03.

Why Silvery Exists

Silvery started from a single frustration: React terminal components can't know their own size during render. In Ink, React renders first, then Yoga calculates layout — so components that need to adapt (truncate text, choose compact vs full layout, fit columns) must use post-render effects or prop drilling. This limitation (Ink #5, open since 2016, still open as of 2026-03) cascades: no native scrolling, no automatic text truncation, no responsive layouts without workarounds.

Fixing it required a different rendering pipeline — layout first, then render — which meant building from scratch. The result is a better renderer: responsive layout, per-node incremental rendering, ANSI-aware compositing, native scrolling, pure TypeScript. On top of this core, optional framework layers provide input management, commands, mouse support, 30+ components, theming, and TEA state machines — use as much or as little as you need.

The Two Projects

Ink (2017) brought React to the terminal. ~1.3M npm weekly downloads, 50+ community components, used by Gatsby, Prisma, Terraform CDK, Shopify CLI, Claude Code, and many more. Mature, stable, actively maintained. Ink is a focused React renderer.

Silvery (2025) is a ground-up reimplementation with a different rendering architecture. At its core, it's a renderer — Box, Text, useInput, render() work the same as Ink. Optional framework layers (@silvery/ui for 30+ components, @silvery/tea for state machines, @silvery/theme for theming) extend it into an ergonomic full-stack toolkit when you want one.

For how Silvery compares to terminal UI frameworks beyond Ink (BubbleTea, Textual, Notcurses, FTXUI, blessed), see comparison.md.

See migration guide for switching from Ink.

Performance numbers in this document are from the Ink comparison benchmark suite. Reproduce with bun run bench for raw benchmark tables.

Compatibility at a Glance

Silvery passes 804/813 of Ink's own test suite (98.9%) when tested with the Flexily layout engine. Chalk compatibility is 32/32 (100%). These numbers come from cloning the real Ink and Chalk repos and running their original test suites against silvery's compat layer (bun run compat).

The 9 remaining Ink test failures break down as:

CategoryFailuresWhy
Flexily layout differences4flex-wrap (2), aspect ratio (2) — W3C spec vs Yoga behavior
Overflow edge cases3overflowX clipping differences
measure-element1Post-state-change re-measurement timing
render-to-string1Effect timing in synchronous render

These are edge cases, not migration blockers. The layout differences come from Flexily following the W3C CSS spec where Yoga diverges — if you prefer browser-standard behavior, these are features, not bugs. If you need exact Yoga layout parity, Silvery supports Yoga as a pluggable layout engine.

The compat layer is built as thin adapters (~50 lines each) that bridge Ink's APIs to silvery-native systems. withInk() composes withInkCursor() + withInkFocus() — you can use them individually or drop them as you adopt silvery-native APIs. See compatibility reference for the full API mapping and compat layer architecture for how the bridge works.

Shared Foundation

Silvery and Ink share the same core ideas -- the migration path is intentionally short:

  • React 19 component model -- JSX, hooks (useState, useEffect, useMemo, etc.), reconciliation, keys
  • Box + Text primitives -- Flexbox layout via <Box> with direction/padding/margin/border, styled text via <Text>
  • Flexbox layout -- Both use CSS-like flexbox (Silvery via Flexily or Yoga, Ink via Yoga WASM)
  • useInput hook -- Same callback signature (input, key) => void for keyboard handling
  • useApp / exit pattern -- useApp() to access app-level methods including exit()
  • Static component -- Render content above the interactive area (log lines, completed items)
  • Spacer / Newline / Transform -- Same utility components
  • Border styles -- single, double, round, bold, classic, etc.
  • measureElement -- Both offer ways to measure rendered elements
  • Layout metrics -- Both provide hooks for element dimensions (useContentRect / useBoxMetrics)
  • Kitty keyboard protocol -- Both support extended modifiers and key event types
  • renderToString -- Both support synchronous string rendering without terminal setup
  • Cursor positioning -- Both provide useCursor() for IME support
  • Screen reader support -- Ink has ARIA roles/states; Silvery has basic support
  • Node.js streams -- Both render to stdout, read from stdin

If your app uses Box, Text, useInput, and basic hooks, it works in both with minimal changes.

Where They Differ

Both are React renderers at the core. The rendering architecture is the primary differentiator. Silvery's optional packages then add framework-level features on top. The differences fall into three categories:

Rendering Architecture

FeatureSilveryInk
Responsive layoutuseContentRect() / useScreenRect() -- synchronous, available during renderuseBoxMetrics() -- post-layout via useEffect, returns 0x0 until first measure (added on master, post-v6.8.0; not yet released as of 2026-03)
Incremental renderingPer-node dirty tracking with 7 independent flags; cell-level buffer diffLine-based diff (opt-in since v6.5.0); unchanged lines skipped, but any change rewrites entire line
ANSI compositingCell-level buffer with proper style stacking; ANSI sequences composed, not passed throughString concatenation; ANSI sequences emitted inline, no compositing layer
Scrollable containersoverflow="scroll" with scrollTo -- framework handles measurement and clippingoverflow supports visible and hidden only; scrolling requires manual virtualization
Dynamic scrollbackuseScrollback -- items graduate from interactive area to terminal history (like Claude Code needs)None -- all items must stay in the render tree
Text truncationAutomatic, ANSI-aware; text clips at Box boundariesManual per-component (#584)
CSS/W3C alignmentFlexbox defaults match W3C spec (flexDirection: row); outlineStyle (CSS outline, no layout impact)Non-standard defaults (flexDirection: column); no outline
Layout enginesFlexily (7 KB, pure JS) or Yoga WASM -- pluggableYoga WASM only (yoga-layout v3)
Render targetsTerminal, Canvas 2D, DOM (experimental)Terminal only
Native dependenciesNone -- pure TypeScriptYoga WASM binary blob (no native compilation, but not pure JS)
Memory profileConstant -- Flexily uses normal JS GCYoga WASM uses a linear memory heap that can grow over long sessions (discussion)
Layout cachingFlexily fingerprints + caches unchanged subtreesFull tree recomputation on every layout pass
Synchronized outputDEC synchronized output (mode 2026) for flicker-free rendering in tmux/ZellijNone
Bracketed pasteusePaste hook with automatic mode togglingusePaste hook (added on master, post-v6.8.0, as of 2026-03)
InitializationSynchronous -- pure TypeScript importAsync WASM loading

Interaction Model

FeatureSilveryInk
Input handlingInputLayerProvider stack with DOM-style bubbling, modal isolation, stopPropagationuseInput only -- flat, all handlers receive all input, no isolation
Focus systemTree-based: scopes, spatial navigation (arrow keys), click-to-focus, useFocusWithinTab-based: useFocus with autoFocus, programmatic focus by ID, no spatial navigation
Command systemwithCommands -- named commands with ID, help text, keybindings, runtime introspectionNone
Keybinding systemwithKeybindings -- configurable, context-aware resolution, macOS symbols (parseHotkey("⌘K"))None
Mouse supportSGR protocol, DOM-style event props (onClick, onMouseDown, onWheel), hit testing, dragNone
TextInputBuilt-in with readline, cursor movement, selectionNone (third-party ink-text-input)
TextAreaMulti-line editing with word wrap, scroll, undo/redo via EditContextNone (#676)
Image rendering<Image> -- Kitty graphics + Sixel with auto-detect and text fallbackNone
ClipboardOSC 52 copyToClipboard/requestClipboard -- works across SSHNone
Hyperlinks<Link> -- OSC 8 clickable URLsOSC 8 hyperlinks (fixed in v6.8.0)
Scrollback modeuseScrollback -- completed items freeze into terminal historyNone -- must keep all items in render tree

Developer Experience

FeatureSilveryInk
Component library30+ built-in (VirtualList, TextArea, SelectList, Table, CommandPalette, ModalDialog, Tabs, TreeView, Toast, Spinner, ProgressBar, Image, SplitView, etc.)5 built-in (Box, Text, Static, Newline, Spacer) + 50+ third-party
TEA state machinesBuilt-in @silvery/tea: pure (action, state) -> [state, effects] reducers with replay, undo, and serializable actionsNone -- React hooks only (Zustand/Redux usable via React, but no TEA integration)
Plugin compositionwithCommands / withKeybindings / withDiagnostics / withRenderNone
TestingBuilt-in @silvery/test: createRenderer + Playwright-style auto-locators, buffer assertions, visual snapshotsink-testing-library (third-party)
Render invariantswithDiagnostics -- verifies incremental render matches fresh renderNone
ScreenshotsbufferToHTML() + Playwright -- programmatic visual captureNone
Theme system@silvery/theme with 38 built-in palettes, semantic color tokens, auto-detectionNone (manual chalk styling)
Unicode utilitiesBuilt-in: 28+ functions for grapheme splitting, display width, CJK detection, ANSI-aware truncationThird-party: string-width, cli-truncate, wrap-ansi, slice-ansi
Console captureBuilt-in <Console /> component (composable, embeddable)patchConsole() (intercept-only)
Resource cleanupusing / Disposable -- automatic teardownManual unmount()
Stream helpersAsyncIterable: merge, map, filter, throttle, debounceNone
AnimationuseAnimation, easing functions, useAnimatedTransitionNone (manual setInterval)
Non-TTY detectionisTTY(), resolveNonTTYMode(), renderString() fallbackTerminal size detection for piped processes (v6.7.0)
Terminal inspectionSILVERY_DEV=1 inspector with tree visualization, dirty flags, focus pathReact DevTools integration
CommunityNewMature ecosystem, ~1.3M npm weekly downloads

Performance

Apple M1 Max, Bun 1.3.9, Feb 2026. Reproduce: bun run bench:compare

Benchmarks measure a specific scenario for each row. "Typical interactive update" = single setState in a mounted 1000-node tree (e.g., moving a cursor). Silvery updates only the dirty subtree; Ink reconciles all nodes.

ScenarioSilveryInk
Cold render (1 component)165 us271 usSilvery 1.6x faster
Cold render (1000 components)463 ms541 msSilvery 1.2x faster
Full React rerender (1000 components)630 ms20.7 msInk 30x faster
Typical interactive update169 us20.7 msSilvery 100x+ faster
Layout (50-node kanban)57 us (Flexily)88 us (Yoga WASM)Flexily 1.5x faster
Terminal resize (1000 nodes)21 usFull re-render--
Buffer diff (80x24, 10% changed)34 usN/A (line-based)--

Understanding the rerender row: When the entire component tree re-renders from scratch (e.g., replacing the root element), Ink is 30x faster because its output is string concatenation. Silvery runs a 5-phase pipeline (measure, layout, content, output) after React reconciliation -- that is the cost of responsive layout. But this scenario rarely happens in real apps.

The row that matters -- "typical interactive update": When a user presses a key (cursor move, scroll, toggle), only the changed nodes need updating. Silvery has per-node dirty tracking that bypasses React entirely -- 169 us for 1000 nodes. Ink's incremental rendering (v6.5.0+) improves output by skipping unchanged lines, but it still re-renders the entire React tree and runs full Yoga layout on every state change -- 20.7 ms. Silvery's dirty tracking skips React reconciliation, layout, and content generation for unchanged nodes -- a fundamentally different approach.

Key Differences Explained

Responsive Layout

The core architectural difference. Ink renders components, then runs Yoga layout. useBoxMetrics() provides dimensions after layout via useEffect, meaning the first render always sees {width: 0, height: 0}. Silvery runs layout first, then renders components with actual dimensions via useContentRect().

tsx
// Ink: useBoxMetrics returns 0x0 on first render, updates via effect
function Card() {
  const ref = useRef(null)
  const { width, hasMeasured } = useBoxMetrics(ref)
  if (!hasMeasured)
    return (
      <Box ref={ref}>
        <Text>Loading...</Text>
      </Box>
    )
  return (
    <Box ref={ref}>
      <Text>{truncate(title, width)}</Text>
    </Box>
  )
}

// Silvery: useContentRect returns actual dimensions immediately
function Card() {
  const { width } = useContentRect()
  return <Text>{truncate(title, width)}</Text>
}

This difference cascades into scrolling, auto-truncation, responsive layouts, and any feature that needs to know "how much space do I have?" during the render pass rather than after it.

Scrolling

Ink's overflow property supports visible and hidden -- not scroll. Scrolling remains the #1 feature request (#222, open since 2019):

tsx
// Ink: manual virtualization with height estimation
<VirtualList
  items={items}
  height={availableHeight}
  estimateHeight={(item) => calculateHeight(item, width)}
  renderItem={(item) => <Card item={item} />}
/>

// Silvery: render everything, let the framework handle it
<Box overflow="scroll" scrollTo={selectedIdx}>
  {items.map(item => <Card key={item.id} item={item} />)}
</Box>

Input Layering

Ink's useInput is flat -- all registered handlers receive all input. Opening a modal dialog means manually checking flags in every handler:

tsx
// Ink: every handler must check modal state
useInput((input, key) => {
  if (isDialogOpen) return  // must guard in EVERY handler
  if (input === 'j') moveDown()
})

// Silvery: input layers isolate automatically
<InputLayerProvider>
  <Board />        {/* receives input when dialog is closed */}
  {isOpen && <Dialog />}  {/* consumes input, board never sees it */}
</InputLayerProvider>

Focus System

Ink provides tab-order focus with useFocus() — components register in a flat list and cycle via Tab/Shift+Tab. Silvery provides tree-based focus with scopes, spatial navigation (arrow keys move focus directionally based on layout position), click-to-focus, useFocusWithin, and DOM-style focus/blur events:

tsx
// Silvery: spatial focus navigation
<FocusScope>
  <Row>
    <FocusableCard /> {/* Left arrow → previous, Right arrow → next */}
    <FocusableCard />
    <FocusableCard />
  </Row>
</FocusScope>

Compat bridge: withInkFocus() provides Ink's flat-list focus system as a thin plugin. Apps using Ink's useFocus() / useFocusManager() work unchanged. For new code, silvery's useFocusable() is strictly better — spatial awareness, focus scopes, event dispatch. See Compat Layer Architecture.

Mouse Support

Silvery implements SGR mouse protocol (mode 1006) with DOM-style event handling:

tsx
// Silvery: DOM-style mouse events
<Box onClick={(e) => selectItem(e.target)} onMouseDown={(e) => startDrag(e)} onWheel={(e) => scroll(e.deltaY)}>
  <Text>Click me</Text>
</Box>

Ink has no mouse support.

Layout Engines

Silvery supports pluggable layout engines with the same flexbox API:

Flexily (default)Yoga (WASM)
Size (gzip)7 KB38 KB
LanguagePure JSC++ -> WASM
InitializationSynchronousAsync
100-node layout85 us88 us
50-node kanban57 us54 us
RTL directionSupportedSupported
Baseline alignmentNot supportedSupported

Both are fast enough for 60fps terminal UIs. Flexily is 5x smaller with comparable performance. See the Flexily docs for details.

Flexily vs Yoga Philosophy

Flexily intentionally follows the W3C CSS Flexbox specification where Yoga diverges from it. These aren't bugs — they're design choices that make Flexily behave like browsers do:

BehaviorFlexily (CSS spec)Yoga (Ink)Why it matters
Default flexDirectionrowcolumnCSS §9.1: initial value is row. Ink chose column for document-flow convenience, but it surprises anyone coming from web CSS.
overflow:hidden + flexShrink:0Item shrinks to fit parentItem expands to content sizeCSS §4.5: overflow containers have min-size: auto = 0. Without this, an overflow:hidden child with 30 lines inside a height-10 parent computes as height 30 — defeating clipping.
alignContent distributionMatches browser behaviorSlightly different spacingMinor differences in how cross-axis space is distributed across flex lines.

If you prefer browser-standard flexbox, use Flexily (the default). If you need exact Ink layout parity, install Yoga and switch:

bash
bun add yoga-wasm-web
tsx
import { render } from "silvery"

await render(<App />, { layoutEngine: "yoga" })

Or set SILVERY_ENGINE=yoga to switch globally without code changes.

Most Ink apps use simple layouts (flexDirection="column", padding, borders) that work identically in both engines. The differences surface with advanced flexbox features like flexWrap, alignContent, and percentage-based flexBasis.

Terminal Protocol Coverage

Silvery implements a comprehensive set of terminal protocols. This matters for cross-terminal compatibility, modern features (images, clipboard, extended keyboard), and correct rendering in multiplexers like tmux and Zellij.

Escape Sequences

CategoryProtocolSilveryInk
SGR Styling16/256/Truecolor, bold, italic, dim, underline, strikethrough, inverseFullFull
Extended UnderlinesISO 8613-6: single, double, curly, dotted, dashed + underline color (SGR 58/59)FullNone
Cursor ControlCUP, CUU/D/F/B, EL, ED, DECSCUSR (block/underline/bar cursors)FullPartial
Scroll RegionsDECSTBM (set/reset), SU/SD (scroll up/down)FullNone

DEC Private Modes

ModeWhatSilveryInk
25 (DECTCEM)Cursor visibilityYesYes
1000 (X10)Basic mouse trackingYesNo
1002Button event tracking (press + drag)YesNo
1004Focus in/out reportingYesNo
1006 (SGR)Extended mouse protocol (large coordinates)YesNo
1049Alternate screen bufferYesYes
2004Bracketed paste modeYesPost-v6.8.0 (as of 2026-03)
2026Synchronized output (flicker-free)YesNo
DECRPMMode query (CSI ? mode $ p)YesNo

OSC Sequences

OSCWhatSilveryInk
0/2Window titleYesNo
4Palette color query/setYesNo
7Directory reporting (shell integration)YesNo
8Hyperlinks (clickable URLs)YesNo
9iTerm2 notificationsYesNo
22Mouse cursor shape (pointer, text, crosshair, etc.)YesNo
52Clipboard access (copy/paste over SSH)YesNo
66Text sizing protocol (Kitty v0.40+, Ghostty)YesNo
99Kitty notificationsYesNo
133Semantic prompt markers (shell integration)YesNo

Keyboard & Input

ProtocolWhatSilveryInk
Kitty keyboardAll 5 flags (disambiguate, events, alternate, all keys, text)FullAdded on master (post-v6.8.0, as of 2026-03)
Modifier detectionShift, Alt, Ctrl, Super/Cmd, Hyper, CapsLock, NumLockFullBasic
Key event typesPress, repeat, releaseFullPress only
Bracketed pasteusePaste hook with auto-enableFullusePaste hook (post-v6.8.0, as of 2026-03)
Focus reportingFocus in/out eventsFullNone

Graphics

ProtocolWhatSilveryInk
Kitty graphicsPNG transmission with chunking, ID-based managementFullNone
SixelRGBA-to-Sixel encoder with color quantizationFullNone
Auto-detectTry Kitty, fall back to Sixel, fall back to text placeholderYesN/A

Terminal Queries

QueryWhatSilveryInk
CPR (DSR 6)Cursor positionYesNo
CSI 14tPixel dimensionsYesNo
CSI 18tText area size (rows/cols)YesNo
DA1/DA2/DA3Device attributesYesNo
XTVERSIONTerminal identificationYesNo

Silvery uses these queries at startup for capability detection — automatically enabling Kitty keyboard, SGR mouse, synchronized output, and other features based on what the terminal supports.

When to Choose What

Choose Ink when:

  • You need a mature ecosystem with community components
  • Your app is a simple CLI prompt (one-shot interaction)
  • You want the safety of a battle-tested, widely-deployed renderer
  • You do not need scrolling, mouse, or complex focus management

Choose Silvery when:

  • You are building a complex interactive TUI (kanban board, text editor, dashboard)
  • You need scrollable containers, mouse support, or spatial focus navigation
  • You want a command system with keybindings and introspection
  • You need components to know their dimensions during render
  • You want multi-target rendering (terminal + canvas + DOM)
  • You care about interactive update performance (dirty tracking vs full re-render)
  • You want a complete component library without assembling third-party packages

Real-World Scenarios

Dashboard with Resizable Panes

Components need to know their dimensions to render content appropriately (charts, tables, wrapped text).

  • Ink: Use useBoxMetrics (post-layout, starts at 0x0). Re-render entire tree on resize.
  • Silvery: Each pane reads useContentRect() and adapts immediately. Resize triggers layout-only pass (21 us for 1000 nodes).

Scrollable Task List

A list of 500+ items where the user navigates with j/k.

  • Ink: Requires manual virtualization with height estimation. overflow only supports visible/hidden.
  • Silvery: overflow="scroll" handles everything. VirtualList component optimizes large lists.

Kanban Board

3+ columns of cards, each column independently scrollable, cards showing truncated content.

  • Ink: Manual scroll per column, manual truncation, width-threading through props.
  • Silvery: Columns and cards auto-size. Each column scrolls independently. Text auto-truncates.

Search with Live Filtering

Type-ahead search with debounced results rendering.

  • Ink: useInput for text capture, manual list rendering. No input isolation between search box and results.
  • Silvery: InputLayerProvider for text input isolation, useContentRect for result count fitting, useDeferredValue for responsive filtering.

Simple CLI Prompt

One-shot question, answer, exit.

  • Ink: Excellent -- large ecosystem of prompt components (ink-select-input, ink-text-input, ink-spinner).
  • Silvery: Built-in TextInput, SelectList, Spinner components. Works, but the community ecosystem is smaller.

Real-World Impact

These are not theoretical differences. Production Ink-based CLIs have encountered several of these limitations:

  • Memory: Large-scale Ink apps have encountered memory growth from Yoga's WASM linear memory, which cannot shrink once allocated (e.g., Claude Code saw its process balloon over time). Silvery avoids this class of problem by using a pure JavaScript layout engine with normal garbage collection.
  • Flicker: Earlier Ink versions cleared the entire terminal area on each render, causing visible flicker, especially in tmux. Ink v6.5.0+ added line-based incremental rendering and v6.7.0 added synchronized updates to mitigate this. Silvery's cell-level dirty tracking and buffer diff provide more granular flicker prevention.
  • Missing capabilities: Production CLIs have needed mouse support, customizable keybindings, scrollable containers, and complex focus management -- features that require additional libraries or manual implementation in Ink but are built into Silvery.

Compatibility Coverage

Tested scenarios derived from common Ink issues:

ScenarioSilvery TestInk Issue
CJK character rendering (Chinese, Japanese, Korean)ime.test.tsx#759
Double-width character alignmentime.test.tsx, wide-char-truncate.test.ts#759
Emoji ZWJ sequencesime.test.tsx--
ANSI-aware text truncationtext-truncate-width.test.ts#584
Rapid keystrokes (burst input)input.test.tsxPR #782
borderDimColorborder-dim-color.test.tsx#840
Large component counts (1000+)performance.test.tsx, memory.test.tsx#694
Home/End key supportkeys.test.tsPR #829
Process exit timingexit.test.tsx#796
tmux renderingterminal-multiplexers.test.ts, sync-update.test.tsPR #846
Zellij renderingterminal-multiplexers.test.tsPR #846

Appendix: Detailed Benchmarks

Apple M1 Max, Bun 1.3.9, Feb 2026. Reproduce: bun run bench:compare

Full Pipeline (React Reconciliation + Layout + Output)

ComponentsSilvery (Flexily)Ink 6 (Yoga WASM)Faster
1 Box+Text (80x24)165 us271 usSilvery 1.6x
100 Box+Text (80x24)45.0 ms49.4 msSilvery 1.1x
1000 Box+Text (120x40)463 ms541 msSilvery 1.2x

Silvery uses createRenderer() (headless). Ink uses render() with mock stdout + unmount per iteration.

React Rerender (Apples-to-Apples)

Both trigger full React reconciliation via app.rerender():

ComponentsSilveryInk 6Faster
100 Box+Text (80x24)64.3 ms2.3 msInk 28x
1000 Box+Text (120x40)630 ms20.7 msInk 30x

Ink is faster because it writes directly to a string buffer. Silvery runs the 5-phase pipeline after reconciliation.

Silvery Dirty-Tracking Update (No Ink Equivalent)

Per-node dirty tracking bypasses React entirely:

NodesFirst RenderDirty UpdateFaster
1311 us38 us8x
10023 ms46 us500x
1000236 ms169 us1396x

This is the typical update path for interactive TUIs (cursor movement, scroll, single-node edits).

Buffer Diff

ScenarioTime
80x24, no changes28 us
80x24, 10% changed34 us
80x24, full repaint59 us
200x50, no changes146 us

Packed Uint32Array cell comparison with cursor-movement optimization.

Layout Engine (Pure Layout, No React)

BenchmarkFlexily (JS)Yoga WASM
100 nodes flat85 us88 us
50-node kanban57 us54 us

Resize (Layout Only)

NodesTime
10250 ns
1002 us
100021 us

Bundle Size

PackageSize (gzip)
Silvery + Flexily~45 KB
Silvery + Yoga~76 KB
Ink~52 KB

Released under the MIT License.