---
url: /getting-started/quick-start.md
---
# Quick Start
Polished terminal apps in React.
Silvery is a React renderer for terminal applications — use it as just a renderer, or add optional packages for a complete framework. The core API (`Box`, `Text`, `useInput`) works like other React terminal renderers. What's different: responsive layout, native scrolling, and per-node incremental rendering.
## Installation
::: code-group
```bash [bun]
bun add silvery
```
```bash [npm]
npm install silvery
```
```bash [yarn]
yarn add silvery
```
```bash [pnpm]
pnpm add silvery
```
:::
> **One package is all you need.** `silvery` includes everything to build terminal apps — components, hooks, and rendering. The `@silvery/*` scoped packages exist for advanced users who want finer-grained imports.
## Your First App
Create a file called `app.tsx`:
```tsx
import { render, Box, Text } from "silvery"
function App() {
return (
Hello from Silvery!
)
}
await render().run()
```
Run it:
```bash
bun app.tsx
# or: npx tsx app.tsx
```
You should see a rounded box with "Hello from Silvery!" inside.
> **Power users:** `render()` and `run()` are sugar over composable plugins. For full control over terminal I/O, focus, events, and commands, use `pipe()` — see [Building an App](/guides/terminal-apps).
## Responsive Layout
Components can query their own dimensions during render — no prop drilling needed:
```tsx
import { render, Box, Text, useBoxRectDangerously } from "silvery"
function SizedBox() {
const { width, height } = useBoxRectDangerously()
return (
I am {width}x{height}
)
}
function App() {
return (
)
}
await render().run()
```
Each `SizedBox` will display its actual computed dimensions. No prop threading needed!
## Interactive Lists
`SelectList` gives you keyboard-navigable selection out of the box — arrow keys, j/k, Home/End, disabled items, and scroll windowing. No manual `useInput()` or cursor state needed:
```tsx
import { render, Box, Text, SelectList } from "silvery"
function App() {
return (
Pick a framework: console.log(`Selected: ${item.value}`)}
maxVisible={5}
/>
)
}
await render().run()
```
For large datasets, use `VirtualList` with `interactive` mode — it renders only visible items and handles keyboard navigation (j/k, Page Up/Down, Home/End):
```tsx
import { render, Box, VirtualList, Text } from "silvery"
const items = Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`)
function App() {
return (
{item} }
onSelect={(index) => console.log(`Selected: ${items[index]}`)}
/>
)
}
await render().run()
```
## Next Steps
* [Components](/guides/components) -- Learn about Box, Text, and other components
* [Terminal Apps](/guides/terminal-apps) -- Build a full terminal application step by step
* [API Reference](/api/box) -- Complete API documentation
---
---
url: /getting-started/migrate-from-ink.md
description: >-
Switch an Ink 7.0 app to Silvery — drop-in @silvery/ink compat layer, then opt
into Silvery-native features at your own pace.
---
# Migrate from Ink
Silvery is a drop-in for Ink. Change one import line and your app keeps running — same `Box`, `Text`, `useInput`, `useApp`, `useFocus`, `usePaste`, `useAnimation`, `useWindowSize`, `useStdout`, `Static`. **918 of Ink 7.0's 931 tests (~98.6%) pass on the compat layer.** Then opt into Silvery-native features (responsive layout, scroll containers, mouse, find, clipboard, 45+ components) one at a time.
## The 60-second migration
::: code-group
```bash [bun]
bun remove ink && bun add silvery react
```
```bash [npm]
npm uninstall ink && npm install silvery react
```
```bash [pnpm]
pnpm remove ink && pnpm add silvery react
```
```bash [yarn]
yarn remove ink && yarn add silvery react
```
:::
Change every `from "ink"` import to `from "silvery/ink"`:
```diff
- import { Box, Text, render, useInput } from "ink"
+ import { Box, Text, render, useInput } from "silvery/ink"
```
That's it. Your app should compile and run.
If you used Chalk, swap that too:
```diff
- import chalk from "chalk"
+ import chalk from "silvery/chalk"
```
The Chalk compat layer passes **32 of 32 tests (100%)**.
## What changes (and what doesn't)
**Nothing changes about your component code.** ``, ``, `useInput`, `useFocus`, `useFocusManager`, `useApp`, `useStdout`, `useStdin`, `useAnimation`, `useWindowSize`, `useCursor`, `usePaste`, `Static`, `Transform`, `render()` — all the same names, same props, same behavior.
**What's different** — 13 intentional design choices in the compat layer:
* **W3C flexbox spec over Yoga quirks** (4 tests) — Silvery's [Flexily](https://beorn.codes/flexily) layout engine follows the W3C spec strictly. Yoga has non-standard behaviors around `flex-wrap` and `aspect-ratio` that some Ink tests expect. If you need exact Yoga parity, [Silvery supports Yoga as a pluggable layout engine](/guide/layout-engine).
* **Build artifact format** (2 tests) — Ink expects a `./build/` directory. Silvery ships TypeScript source (for Bun) plus pre-built `dist/` (for Node).
* **Minor rendering edge cases** (~7 tests) — SGR attribute emission order (dim+bold), `measureElement` timing in synchronous render, `renderToString` effect ordering. Produces identical visual output.
[Full compatibility table →](/guide/silvery-vs-ink#compatibility)
## What you gain (opt-in, when you're ready)
Once the import swap works, you can adopt Silvery-native features incrementally. None of these break your existing Ink-style code.
### Switch one component to native imports
```diff
- import { Box, Text } from "silvery/ink"
+ import { Box, Text } from "silvery"
```
The native `silvery` exports are the same `Box` and `Text` — the compat layer is a thin shim. Imports from `silvery` give you access to the broader API (`useBoxRectDangerously`, native focus, scroll containers, etc.) in the same file.
### Responsive layout — `useBoxRectDangerously()` returns real dimensions during render
In Ink, `useBoxMetrics()` returns `{ width: 0, height: 0 }` on the first render and updates via `useEffect`. That means N nesting levels = N visible flickers.
Silvery's `useBoxRectDangerously()` returns actual dimensions on the first render — no `width: 0` flash:
```tsx
import { Box, Text, useBoxRectDangerously } from "silvery"
function Card({ title }: { title: string }) {
const { width } = useBoxRectDangerously()
return (
{width >= 32 ? title : title.slice(0, width - 1) + "…"}
)
}
```
[Responsive Layout guide →](/guide/responsive-layout)
### Scroll containers — `overflow="scroll"` with virtualization
Ink core has `visible` / `hidden` only ([#222](https://github.com/vadimdemedes/ink/issues/222), open since 2019). Silvery has true scroll containers with virtualization, sticky positioning, and native arrow-key/wheel scrolling:
```tsx
```
[Scrolling guide →](/guide/scrolling) · [Sticky positioning →](/guide/layout-coordinates)
### Mouse, text selection, find, clipboard
Ink has none of these in core. Silvery ships them all:
* **Mouse events** — `onClick`, `onWheel`, drag-and-drop, hit testing. [Event Handling →](/guide/event-handling)
* **Text selection** — mouse drag, word/line selection, `userSelect` boundaries. [Text Selection →](/guide/text-selection)
* **Find** — `Ctrl+F` with match highlighting and `n`/`N` navigation. [Find guide →](/guide/find)
* **Clipboard** — OSC 52 paste/copy, `Esc, v` copy-mode for vim-style yanking. [Clipboard guide →](/guide/clipboard)
### 45+ built-in components
Ink core has 6 components. The [`@inkjs/ui`](https://github.com/vadimdemedes/ink-ui) package adds 13 more. Silvery ships 45+ in core — `SelectList`, `TextInput`, `TextArea`, `Table`, `TreeView`, `Tabs`, `CommandPalette`, `ModalDialog`, `PickerDialog`, `Toast`, `Spinner`, `ProgressBar`, `Form`, `SplitView`, and more. Every component participates in focus, mouse, and keybindings automatically.
[Components overview →](/guides/components)
### Performance — 3–27× faster on mounted rerenders
Cell-level dirty tracking, per-node skip for unchanged subtrees, cell-level buffer diff. Native scrollback preserved in inline mode. Typical interactive update on a 1000-node tree: ~169 microseconds.
[Benchmarks →](/guide/silvery-vs-ink#performance)
## What about Ink plugins?
Some Ink ecosystem packages (`ink-select-input`, `ink-text-input`, `ink-spinner`, etc.) work unchanged against `silvery/ink`. Others rely on Ink's private DOM-tree internals and may need a compat shim or replacement.
When swapping an Ink ecosystem component, check if Silvery has a native equivalent first — it usually does, and the native version is faster and more featureful.
| Ink ecosystem | Silvery native | Notes |
| ---------------------- | ------------------- | -------------------------------------------------------------------------- |
| `ink-select-input` | `SelectList` | Search, multi-select, virtualization, keyboard + mouse |
| `ink-text-input` | `TextInput` | Readline binding, full Unicode, paste, IME |
| `ink-spinner` | `Spinner` | Built-in; `Spinner.start("…")` CLI helper too |
| `ink-table` | `Table` | Responsive columns, search/filter |
| `ink-progress-bar` | `ProgressBar` | Full-featured, theme-aware |
| `ink-syntax-highlight` | `SyntaxHighlighter` | Use `languageForPath` from `@silvery/syntax` when inferring from filenames |
## Want the full feature comparison?
The [Silvery vs Ink](/guide/silvery-vs-ink) page has the full feature matrix, performance numbers, compatibility breakdown, and migration scenarios:
* [Compatibility coverage](/guide/silvery-vs-ink#compatibility) — exactly which Ink tests pass, which 13 fail and why
* [Performance & size](/guide/silvery-vs-ink#performance-size) — benchmarks for mounted rerender, cold render, bundle
* [Compat Layer Architecture](/reference/compatibility#compat-layer-architecture) — how the adapters bridge Ink APIs to silvery-native systems
## Need help?
* Open an [issue on GitHub](https://github.com/beorn/silvery/issues) — happy to look at your migration
* See [Troubleshooting](/guide/troubleshooting) for common gotchas
* Browse [Imports & Subpaths](/guide/imports) for the full mapping between `silvery`, `silvery/ink`, `silvery/chalk`, and `silvery/runtime`
---
---
url: /guide/the-silvery-way.md
---
# The Silvery Way
*How to build the shiniest Silvery apps*
Silver tarnishes when you don't take care of it. So does your code. These are the principles that keep Silvery apps **shiny** (best practices) — and the anti-patterns that let them go **tarnished** (common pitfalls).
Each principle is something people get wrong. If you're coming from Ink, Blessed, or raw ANSI — we've all been there. These patterns are natural starting points, but Silvery gives you better tools. If you're already doing the shiny thing, skip ahead.
## Three foundations
The ten principles below all flow from three broader convictions:
1. **Take the best from the web.** Flexbox, scroll containers, DOM-style events, focus scopes, Playwright-style testing, design tokens — thirty years of web UI produced ideas worth keeping. If you'd reach for it on the web, reach for it in Silvery.
2. **Stay true to the terminal.** Cells, screens, buffers, ANSI, scrollback. The terminal is the medium, and Silvery embraces it. When a feature maps onto a terminal protocol — Kitty keyboard, OSC 52 clipboard, DEC mode 2026 — we expose it honestly, not as a polyfill.
3. **Raise the bar.** For developer ergonomics, architecture composability, and performance. The ten principles below are what this looks like in practice — each one exists because a shiny path is worth building and maintaining.
## 1. Use the Built-in Components
Silvery ships 45+ components — all available from `import { ... } from "silvery"`. They handle keyboard navigation, theming, mouse support, kill ring, word movement, scroll indicators, and dozens of edge cases you haven't thought of yet. When you reimplement them, you lose all of that.
::: tip ✨ Shiny
```tsx
Are you sure?
```
All themed. All mouse-aware. All keyboard-navigable out of the box — saving you time and bugs.
:::
::: danger 🩶 Tarnished
```tsx
// Manual cursor tracking — loses j/k wrapping, mouse, scroll, selection theming
const [cursor, setCursor] = useState(0)
useInput((input) => {
if (input === "j") setCursor((c) => Math.min(c + 1, items.length - 1))
if (input === "k") setCursor((c) => Math.max(c - 1, 0))
})
// Manual text handling — no Ctrl+A/E/K/U/W, no Alt+B/F, no kill ring, no clipboard
useInput((input, key) => {
if (key.backspace) setText((t) => t.slice(0, -1))
else if (!key.ctrl) setText((t) => t + input)
})
```
You'll spend a week reimplementing what [`SelectList`](/guides/components#selectlist) and [`TextInput`](/guides/components#textinput) give you in one line.
:::
→ [Components guide](/guides/components) · [Components & hooks reference](/reference/components-hooks)
## 2. Think in Flexbox
Silvery uses CSS flexbox via [Flexily](/guide/layout-engine) — same mental model as web development. Let the layout engine compute positions and sizes. For responsive layout, reach for [`useResponsiveBoxProps`](/api/use-responsive-box-props) (declarative, no measurement reads). When you genuinely need a component's measured size, [`useBoxRectDangerously()`](/api/use-box-rect) returns the deferred (committed) rect — invariant across every convergence pass within one event batch, so layout decisions can't form feedback loops.
::: tip ✨ Shiny
```tsx
// Fill remaining space
I expand
// Spacing with gap, not margins on every child
// Responsive layout — declarative, no measurement reads.
function AppShell({ sidebar, main }) {
const layout = useResponsiveBoxProps({
default: { flexDirection: "column" },
md: { flexDirection: "row" },
})
return {main}
}
// Measured-rect decisions — when the parent's measured size is the input.
function Panel() {
const { width } = useBoxRectDangerously() // committed rect, batch-invariant
return width < 40 ? :
}
```
`flexGrow` fills space. `padding`/`paddingX` for internal spacing. `gap` between children. `justifyContent="flex-end"` pins to bottom. `useResponsiveBoxProps` for global-viewport responsive layout. `useBoxRectDangerously()` when the decision depends on the parent's measured size.
:::
::: tip ⚠ Reading layout on mount
The reactive layout hooks (`useBoxRectDangerously`, `useScrollRect`, `useScreenRect`) return the COMMITTED rect — the value as of the most recent event-batch commit. On the very first render the rect is `{0,0,0,0}`; the measured value arrives one batch later. For layout decisions that need to be correct on the first paint, prefer `useResponsiveBoxProps` — it reads the global viewport directly, no layout-pass dependency.
For framework primitives that must publish positioned terminal escapes on the same frame the host node lays out (e.g. `Image` writing Kitty graphics escapes), read `node.boxRect` from `useAgNode()` inside a `useLayoutEffect`. That's the in-flight value, written every layout pass. This is recommended only for leaf primitives in silvery itself, not application code.
:::
::: danger 🩶 Tarnished
```tsx
// Manual arithmetic — breaks on resize, breaks with padding, breaks with borders
// Fake padding with spaces
{" "}Hello{" "}
// Prop-drilling dimensions through 5 components
```
If you're doing arithmetic with widths, you're fighting the layout engine instead of using it.
:::
→ [Layout engine](/guide/layout-engine) · [Responsive layout](/guide/responsive-layout) · [useBoxRectDangerously](/api/use-box-rect) · [useResponsiveBoxProps](/api/use-responsive-box-props) · [Box](/api/box) · [Layout examples](/examples/layout)
## 3. Let the Framework Scroll
`overflow="scroll"` measures children, determines visibility, renders only what fits, responds to wheel input, and shows scroll indicators. It handles variable heights. Supplying `onWheel` replaces the default kinetic scroll owner for specialized or virtualized surfaces.
::: tip ✨ Shiny
```tsx
// Automatic scrolling — just tell it what to keep visible
{items.map((item, i) => )}
// Streaming content — chat, logs, agent output
{messages}
// Clip without scroll indicators
{longContent}
```
:::
::: danger 🩶 Tarnished
```tsx
// Manual slicing — wrong heights, no indicators, no variable-height items
const visible = items.slice(scrollOffset, scrollOffset + pageSize)
// ... 40 lines of offset tracking, boundary clamping, page-up/page-down ...
{
scrollOffset > 0 && ▲ {scrollOffset} more
}
```
Manual scroll offset tracking is the #1 source of off-by-one bugs in terminal apps. Let the framework handle it.
:::
→ [Scrolling guide](/guide/scrolling) · [Scroll regions reference](/reference/scroll-regions) · [Scrollback examples](/examples/scrollback)
## 4. Focus Control
[`silvery`](/reference/packages) provides tree-based focus with spatial navigation. Focus determines which component receives input. Without focus management, overlapping key handlers create chaos — a modal opens but the background still handles keys.
::: tip ✨ Shiny
```tsx
// Calling useFocus() registers this component in the focus tree
function SearchBox() {
const { isFocused } = useFocus() // isFocused can be used for styling
return
}
// Input layer stack — modals automatically consume input
;
{/* No guards needed — background input is automatically blocked */}
This cannot be undone.
// Programmatic focus navigation
focusNext() // Tab-like cycling
focusPrev() // Shift-Tab
setFocus(id) // Jump to specific component
```
:::
::: danger 🩶 Tarnished
```tsx
// Guard clauses in every handler — easy to forget one
useInput((input) => {
if (isDialogOpen) return // Forgot this? Background responds to keys
if (isSearching) return // Another guard
if (isPanelOpen) return // And another
// actual logic buried under guards
})
// Manual focus boolean threaded through props
```
If you're writing `if (isDialogOpen) return` in your input handlers, you don't have focus management — you have guard clauses pretending to be focus management.
:::
→ [Focus hooks](/api/use-focus) · [Input features](/reference/input-features)
## 5. Command System
Named, serializable, introspectable actions. Commands make your app automatable (AI agents invoke commands by name), testable (fire commands in tests), and discoverable ([`CommandPalette`](/guides/components#shadcn-style-components) lists them all).
> Part of [`@silvery/create`](https://www.npmjs.com/package/@silvery/create) — the optional app architecture layer. `@silvery/create` is under active development; the command system API is evolving.
::: tip ✨ Shiny
```tsx
// Declare commands with metadata
const app = withCommands({
"file.save": { label: "Save File", handler: save },
"file.open": { label: "Open File", handler: open },
"view.toggle-sidebar": { label: "Toggle Sidebar", handler: toggleSidebar },
})
// Keybindings reference command names
withKeybindings({
"ctrl+s": "file.save",
"ctrl+o": "file.open",
"ctrl+b": "view.toggle-sidebar",
})
// AI agent can invoke by name
agent.executeCommand("file.save")
// All commands discoverable via palette
```
:::
::: danger 🩶 Tarnished
```tsx
// Anonymous handlers — can't list, can't replay, can't automate
useInput((input, key) => {
if (key.ctrl && input === "s") save()
if (key.ctrl && input === "o") open()
if (key.ctrl && input === "b") toggleSidebar()
})
// AI agent forced to simulate keypresses
// No way to list available actions
// No command palette possible
```
Keybindings are a UI detail. Commands are the API. Build on commands; bind keys to them.
:::
→ [Event handling](/guide/event-handling)
## 6. Style using Design Tokens
Components express semantic intent — color, hierarchy, state — via **tokens and presets**. The token system decides the concrete rendering details: which hex value, which SGR attributes, which tier-appropriate fallback. Components never reach for raw rendering primitives (hex colors, ANSI escapes, or SGR modifiers like `dim` / `bold` / `italic` / `underline`).
[`silvery/theme`](/reference/packages) auto-detects your terminal's palette via OSC queries — no configuration needed. [Sterling](/reference/sterling) — silvery's canonical design system — derives nested roles + flat tokens (`$fg-accent`, `$fg-muted`, `$bg-surface-*`, `$border-focus`, …) plus typography presets that adapt to every terminal theme. 84 color schemes (Catppuccin, Nord, Dracula, Tokyo Night, Solarized, …) work automatically.
### Why not SGR in components
Terminal SGR codes (`bold`, `dim`, `italic`, `underline`, `inverse`, `strikethrough`) have **uneven support** across emulators — `dim` does alpha-blending on some, intensity-reduction on others, nothing on older terminals. `bold` sometimes brightens color, sometimes only affects font weight. Writing these in components guarantees inconsistent results.
Tokens avoid this. At truecolor, `$fg-muted` resolves to a specific pre-dimmed hex — deterministic on any terminal. At ANSI 16 where we can't express intermediate intensities, the renderer emits SGR 2 as a necessary concession. Components never make that choice; derivation does.
::: tip ✨ Shiny
```tsx
// Semantic tokens — adapt to any theme automatically
Selected item✓ Saved✗ FailedLast modified 2h ago
// Typography presets — semantic intent, not manual attrs
Page title
// $fg-accent + bold (composed by preset)
urgent // bold
aside // italic
fine print // pre-dimmed $fg-muted at truecolor
clickable // $fg-link + brightened, underlined hover affordance
// Status indicators: shape + color (colorblind-safe)
✓ // done
○ // pending
✗ // failed
```
:::
### Interactive treatments are semantic too
Use the shared interaction language instead of assembling hover colors at the call site:
```tsx
const interaction = useInteractionTreatment("control", "surfaceHover")
Open
```
Roles own reveal policy: content links reveal their destination on Cmd/Super-hover, controls reveal on plain hover, and regions reveal on plain hover with a structural gutter cue. `` and action-only `` derive the first two roles automatically, while both gain `$fg-link-hover` plus an underline on plain hover. An explicit `underline` choice wins. Use `` for a self-contained regional treatment, or pass `interactionTreatment` when a parent owns the hover state.
Named surfaces are the normal path. Exported recipe factories such as `actionFill()`, `textPair()`, `cardOutline()`, and `togglePillSurface()` centralize parameterized families; only genuinely runtime-computed visuals use `customInteractionSurface()`. Raw surface objects are rejected by the resolver types so a new one-off treatment cannot silently reintroduce the old pattern.
`DocumentView` also resolves style precedence semantically. A heading's
structural foreground wins over element foregrounds inside it, while link
decoration remains visible through the `link` typography variant's dotted
underline. A selected block's background wins over nested element backgrounds
such as inline code. Adapters provide semantic blocks and inline elements; they
do not manually reapply heading or selection colors.
::: danger 🩶 Tarnished
```tsx
// Hardcoded colors — wrong in light themes, wrong in high-contrast
ErrorError
// ANSI escapes in strings — bypasses the theme entirely
console.log("\x1b[31;1mError\x1b[0m")
// Raw SGR attrs in component code — unreliable across terminals
Last modified 2h ago // → use $fg-muted or
Warning // → use