Skip to content

Components

Box

Flexbox container with borders, padding, and overflow control.

tsx
<Box flexDirection="column" padding={1} borderStyle="single">
  <Text>Content</Text>
</Box>

Box supports all standard flexbox props: flexDirection, flexGrow, flexShrink, flexBasis, alignItems, alignSelf, justifyContent, flexWrap, width, height, minWidth, minHeight, maxWidth, maxHeight, padding, paddingX, paddingY, margin, gap, borderStyle, borderColor, overflow.

Outline Props

Box supports outline props — the CSS outline equivalent. Unlike borderStyle, which adds border dimensions to the layout (making the content area smaller), outline renders border characters that overlap the content area. The layout engine sees no border at all — outline is purely visual.

tsx
<Box outlineStyle="single" outlineColor="cyan">
  <Text>Framed without layout impact</Text>
</Box>
PropTypeDescription
outlineStyleBorderStyleOutline border style (single, double, round, bold, etc.)
outlineColorstringForeground color for the outline
outlineDimColorbooleanApply dim styling to the outline

Use outline when you need to visually frame a box without shifting its content or affecting the layout of sibling elements.

Focus Props

Box supports tree-based focus management via the following props:

PropTypeDescription
focusablebooleanNode can receive focus (required for useFocusable())
autoFocusbooleanFocus this node on mount
focusScopebooleanCreates a focus scope (Tab cycles within this subtree)
nextFocusUpstringtestID to focus when pressing Up from this node
nextFocusDownstringtestID to focus when pressing Down from this node
nextFocusLeftstringtestID to focus when pressing Left from this node
nextFocusRightstringtestID to focus when pressing Right from this node
tsx
<Box testID="panel" focusable autoFocus borderStyle="single">
  <Text>This panel receives focus on mount</Text>
</Box>

<Box testID="scope" focusScope>
  <Box testID="a" focusable><Text>A</Text></Box>
  <Box testID="b" focusable><Text>B</Text></Box>
  {/* Tab cycles between A and B within this scope */}
</Box>

Mouse Event Props

Box and Text support DOM-compatible mouse events:

PropEvent TypeBubbles
onClickSilveryMouseEventYes
onDoubleClickSilveryMouseEventYes
onMouseDownSilveryMouseEventYes
onMouseUpSilveryMouseEventYes
onMouseMoveSilveryMouseEventYes
onMouseEnterSilveryMouseEventNo
onMouseLeaveSilveryMouseEventNo
onWheelSilveryWheelEventYes

Focus Event Props

PropEvent TypeDescription
onFocusSilveryFocusEventCalled when this node gains focus
onBlurSilveryFocusEventCalled when this node loses focus
onKeyDownSilveryKeyEventCalled on key down (bubble phase)
onKeyUpSilveryKeyEventCalled on key up (bubble phase)
onKeyDownCaptureSilveryKeyEventCalled on key down (capture phase)

Scrollable Containers

tsx
<Box overflow="scroll" height={10} scrollTo={selectedIndex}>
  {items.map((item, i) => (
    <Text key={i}>{item.name}</Text>
  ))}
</Box>

Sticky Bottom (Outside Scroll Containers)

stickyBottom works outside scroll containers as a bottom-pinning primitive. The element stays in normal flow but pins to the parent's bottom edge when content is short.

┌──────────────────────┐    ┌──────────────────────┐
│ Content line 1       │    │ Content line 1       │
│                      │    │ Content line 2       │
│                      │    │ Content line 3       │
│                      │    │ Content line 4       │
│ Footer ◄── pinned    │    │ Footer ◄── natural   │
└──────────────────────┘    └──────────────────────┘
  Short content               Full content
tsx
<Box height={5} flexDirection="column">
  <Text>Content</Text>
  <Box position="sticky" stickyBottom={0} height={1}>
    <Text>Footer — always at bottom</Text>
  </Box>
</Box>

Use stickyBottom={N} to offset N rows from the bottom edge.

When the parent has no explicit height (auto-sizes), sticky is a no-op — the child stays at its natural position since parent height equals content height.

Text

Styled text with auto-truncation.

tsx
<Text color="green" bold>Success</Text>
<Text underlineStyle="curly" underlineColor="red">Error</Text>

Text Style Props

PropTypeDescription
colorstringForeground color (named, hex, or rgb())
backgroundColorstringBackground color
boldbooleanBold text
dimbooleanDimmed text
italicbooleanItalic text
underlinebooleanSimple underline
underlineStyleUnderlineStyle'single' | 'double' | 'curly' | 'dotted' | 'dashed'
underlineColorstringUnderline color (independent of text color)
strikethroughbooleanStrikethrough text
inversebooleanSwap foreground/background

Text auto-truncates by default. Use wrap="wrap" to wrap instead, or wrap="overflow" to allow overflow.

VirtualList

Efficient rendering for large lists (100+ items). Only renders visible items.

tsx
<VirtualList
  items={cards}
  height={20}
  itemHeight={1}
  scrollTo={selectedIndex}
  renderItem={(card, index) => <Text key={card.id}>{card.name}</Text>}
/>

Virtualized Items (Scrollback)

VirtualList supports a virtualized prop that excludes a contiguous prefix of items from rendering. Pair with useScrollback to push completed items to terminal scrollback:

tsx
const frozenCount = useScrollback(items, {
  frozen: (item) => item.complete,
  render: (item) => `  ✓ ${item.title}`,
})

<VirtualList
  items={items}
  virtualized={(item) => item.complete}
  renderItem={(item) => <Text>{item.title}</Text>}
/>

Static

Renders content once above the dynamic output. Useful for completed items in a stream.

tsx
<Static items={completedTasks}>{(task) => <Text key={task.id}>✓ {task.name}</Text>}</Static>

Viewport Architecture

Four composable primitives for different rendering modes.

Screen

Fullscreen root component. Claims the full terminal dimensions for flexbox layout.

tsx
<Screen>
  <Sidebar />
  <MainContent />
  <StatusBar />
</Screen>
PropTypeDefaultDescription
flexDirectionstring"column"Flex direction for layout

ScrollbackView

Native scrollback root component. Items flow vertically and transition through Live → Virtualized → Static as they scroll off-screen. Uses useScrollbackItem() for per-item lifecycle control.

tsx
<ScrollbackView items={tasks} keyExtractor={(t) => t.id} isFrozen={(t) => t.done} footer={<Text>Status bar</Text>}>
  {(task) => <TaskItem task={task} />}
</ScrollbackView>
PropTypeDefaultDescription
itemsT[]requiredItems to render
children(item, index) => ReactNoderequiredRender function
keyExtractor(item, index) => string | numberrequiredUnique key per item
isFrozen(item, index) => booleanData-driven freeze predicate
footerReactNodePinned footer
footerHeightnumberDeprecated — footer auto-sizes to content
maxHistorynumber10000Max lines in dynamic scrollback
markersboolean | objectOSC 133 semantic markers
widthnumberstdout.columnsTerminal width (enables resize re-emission)
stdout{ write(data: string): boolean }process.stdoutOutput stream for frozen items

VirtualView

App-managed scrolling within a Screen rectangle. Items mount/unmount based on scroll position.

tsx
<Screen>
  <Header />
  <VirtualView
    items={logs}
    height={20}
    estimateHeight={3}
    scrollTo={selectedIndex}
    renderItem={(item) => <LogEntry data={item} />}
  />
  <StatusBar />
</Screen>
PropTypeDefaultDescription
itemsT[]requiredItems to render
heightnumberrequiredViewport height in rows
renderItem(item, index) => ReactNoderequiredRender function
estimateHeightnumber | (index) => number1Item height estimate
scrollTonumberIndex to scroll to
overscannumber5Extra items beyond viewport
maxRenderednumber100Max items to render
scrollPaddingnumber2Edge padding before scrolling
overflowIndicatorbooleanfalseShow ▲N/▼N indicators
keyExtractor(item, index) => string | numberKey extractor
onEndReached() => voidCalled near end of list (infinite scroll)
onEndReachedThresholdnumber5Items from end to trigger callback

useVirtualizer

Headless virtualization engine shared by ScrollbackView and VirtualView. Count-based API inspired by TanStack Virtual.

tsx
const { range, scrollToItem, getKey } = useVirtualizer({
  count: items.length,
  estimateHeight: 3,
  viewportHeight: 20,
  scrollTo: selectedIndex,
  overscan: 5,
})

Console

Captures console.log / console.error output and renders it as a component.

tsx
import { render, Console, patchConsole } from "@silvery/term"

function App({ console: patched }) {
  return (
    <Box flexDirection="column">
      <Console console={patched} />
      <Text>Status: running</Text>
    </Box>
  )
}

using patched = patchConsole(console)
await render(<App console={patched} />, term)

TextInput

Basic text input with onChange/onSubmit:

tsx
import { TextInput } from "@silvery/term"

function Search() {
  return (
    <TextInput
      value={query}
      onChange={setQuery}
      onSubmit={(value) => console.log("Submitted:", value)}
      placeholder="type here..."
    />
  )
}

TextArea

Multi-line text input with word wrapping, scrolling, and cursor movement.

tsx
import { TextArea } from "@silvery/term"

const [value, setValue] = useState("")
<TextArea
  value={value}
  onChange={setValue}
  onSubmit={(val) => console.log("Submitted:", val)}
  height={10}
  placeholder="Type here..."
/>
PropTypeDefaultDescription
valuestring-Current value (controlled)
defaultValuestring""Initial value (uncontrolled)
onChange(value: string) => void-Called when value changes
onSubmit(value: string) => void-Called on submit
submitKey"ctrl+enter" | "enter" | "meta+enter""ctrl+enter"Key combo to trigger submit
placeholderstring""Placeholder text when empty
isActiveboolean-Whether input is focused (overrides focus system)
heightnumber-Visible height in rows (required)
cursorStyle"block" | "underline""block"Cursor style when unfocused
scrollMarginnumber1Context lines above/below cursor when scrolling
disabledbooleanfalseWhen true, ignore all input and dim text
maxLengthnumber-Maximum number of characters allowed
testIDstring-Test ID for focus system identification

Text selection is always enabled — use Shift+Arrow, Ctrl+Shift+Arrow (word-wise), Shift+Home/End, and Ctrl+A (select all). Typing or deleting with an active selection replaces the selected text.

Keyboard shortcuts: Arrow keys, Home/End, Ctrl+Home/End (document start/end), Ctrl+A/E (line start/end), Alt+B/F (word movement), Ctrl+K/U (kill line), Ctrl+Y (yank), PageUp/PageDown, Backspace/Delete.

Renders a terminal hyperlink using OSC 8 escape sequences. In supporting terminals (iTerm2, Ghostty, Kitty, etc.), the text is clickable. Also registers an onClick handler for mouse-driven interaction within Silvery.

tsx
import { Link } from "@silvery/term"

<Link href="https://example.com">Visit Example</Link>
<Link href="https://example.com" color="green">Green Link</Link>
<Link href="km://node/abc123" onClick={(e) => navigate(e)}>Internal Link</Link>
PropTypeDescription
hrefstringURL (http/https, or custom scheme)
childrenReactNodeLink text content
colorstringText color (default: "blue")
underlinebooleanUnderline the link (default: true)
onClick(event: SilveryMouseEvent) => voidClick handler (preventDefault to skip navigation)
testIDstringTest ID for locator queries

Transform

Applies a string transformation to each line of rendered text output. Compatible with Ink's Transform component.

tsx
import { Transform, Text } from "@silvery/term"

// Uppercase all text
<Transform transform={output => output.toUpperCase()}>
  <Text>Hello World</Text>
</Transform>

// Add line numbers
<Transform transform={(line, index) => `${index + 1}: ${line}`}>
  <Text>First line{'\n'}Second line</Text>
</Transform>
PropTypeDescription
transform(line: string, index: number) => stringFunction applied to each line of output
childrenReactNodeText content to transform

The transform should not change the dimensions of the output (e.g., adding characters that change line width) — otherwise layout will be incorrect.

Image

Renders a bitmap image in the terminal using Kitty graphics or Sixel protocol, with automatic protocol detection and text fallback.

tsx
import { Image } from "@silvery/term"

// From a PNG buffer
<Image src={pngBuffer} width={40} height={15} />

// From a file path
<Image src="/path/to/image.png" width={60} />

// With explicit protocol and custom fallback
<Image src={data} protocol="sixel" fallback="[photo]" />
PropTypeDescription
srcBuffer | stringPNG image data (Buffer) or file path to a PNG file
widthnumberWidth in terminal columns (default: available width from layout)
heightnumberHeight in terminal rows (default: half the width for rough aspect ratio)
fallbackstringText to display when image rendering is not supported (default: "[image]")
protocol"kitty" | "sixel" | "auto"Which protocol to use (default: "auto" — tries Kitty, then Sixel, then fallback)

The component operates in two phases: during layout it renders a Box that reserves the visual space, then after render it writes the image escape sequence directly to stdout, positioned over the reserved space.

Protocol detection helpers:

tsx
import { isKittyGraphicsSupported, isSixelSupported } from "@silvery/term"

if (isKittyGraphicsSupported()) {
  /* Kitty graphics available */
}
if (isSixelSupported()) {
  /* Sixel available */
}

Low-level encoding functions:

tsx
import { encodeKittyImage, deleteKittyImage } from "@silvery/term"
import { encodeSixel } from "@silvery/term"

const kittySeq = encodeKittyImage(pngBuffer, { id: 1, cols: 40, rows: 15 })
const deleteSeq = deleteKittyImage(1)
const sixelSeq = encodeSixel({ pixels, width: 320, height: 240 })

Spinner

An animated loading spinner with multiple built-in styles.

tsx
import { Spinner } from "@silvery/term"

<Spinner />
<Spinner type="arc" label="Loading..." />
<Spinner type="bounce" interval={120} />
PropTypeDescription
type"dots" | "line" | "arc" | "bounce"Spinner style preset (default: "dots")
labelstringLabel text shown after spinner
intervalnumberAnimation interval in ms (default: 80)

ProgressBar

A terminal progress bar with determinate and indeterminate modes.

tsx
import { ProgressBar } from "@silvery/term"

<ProgressBar value={0.5} />
<ProgressBar value={0.75} color="green" label="Downloading..." />
<ProgressBar />  {/* indeterminate (animated bounce) */}
PropTypeDescription
valuenumberProgress 0-1 (omit for indeterminate)
widthnumberWidth in columns (default: available via layout)
fillCharstringFill character (default: "█")
emptyCharstringEmpty character (default: "░")
showPercentagebooleanShow percentage label (default: true if value set)
labelstringLabel text before the bar
colorstringColor of the filled portion

SelectList

A keyboard-navigable single-select list with controlled and uncontrolled modes.

tsx
import { SelectList } from "@silvery/term"

const items = [
  { label: "Apple", value: "apple" },
  { label: "Banana", value: "banana" },
  { label: "Cherry", value: "cherry", disabled: true },
]

<SelectList items={items} onSelect={(opt) => console.log(opt.value)} />
PropTypeDescription
itemsSelectOption[]List of options ({ label, value, disabled? })
highlightedIndexnumberControlled: current highlighted index
onHighlight(index: number) => voidCalled when highlight changes
onSelect(option: SelectOption, index: number) => voidCalled on Enter to confirm selection
initialIndexnumberInitial index for uncontrolled mode
maxVisiblenumberMax visible items (scrolls the rest)
isActivebooleanWhether this list captures input (default: true)

Keyboard: j/Down to move down, k/Up to move up, Enter to select, Ctrl+A for first, Ctrl+E for last. Disabled items are skipped.

Table

A data table with headers, column alignment, and auto-sized columns.

tsx
import { Table } from "@silvery/term"

function Users() {
  return (
    <Table
      columns={[
        { header: "Name", key: "name" },
        { header: "Age", key: "age", align: "right" },
      ]}
      data={[
        { name: "Alice", age: 30 },
        { name: "Bob", age: 25 },
      ]}
    />
  )
}
PropTypeDescription
columnsTableColumn[]Column definitions ({ header, key?, width?, align? })
dataArray<Record<string, unknown> | unknown[]>Data rows (objects or arrays)
showHeaderbooleanShow header row (default: true)
separatorstringColumn separator (default: " │ ")
headerBoldbooleanBold header text (default: true)

Column align supports "left" (default), "right", and "center". Columns auto-size to fit content when width is omitted.

Badge

A small inline label for status display.

tsx
import { Badge } from "@silvery/term"

<Badge label="Active" variant="success" />
<Badge label="Warning" variant="warning" />
<Badge label="Custom" color="magenta" />
PropTypeDescription
labelstringBadge text
variant"default" | "primary" | "success" | "warning" | "error"Color variant (default: "default")
colorstringCustom color (overrides variant)

Divider

A horizontal separator line with optional centered title.

tsx
import { Divider } from "@silvery/term"

<Divider />
<Divider title="Section" />
<Divider char="=" width={40} />
PropTypeDescription
charstringCharacter to repeat (default: "─")
titlestringTitle text centered in the divider
widthnumberWidth in columns (default: available via layout)

Newline

Inserts a line break:

tsx
<Text>First line</Text>
<Newline />
<Text>Second line</Text>

Spacer

Fills available space (equivalent to flexGrow: 1):

tsx
<Box>
  <Text>Left</Text>
  <Spacer />
  <Text>Right</Text>
</Box>

Released under the MIT License.