Skip to content

Tables & Data

Terminal data explorers need to handle thousands of rows, resize gracefully across terminal widths, and respond instantly to search queries. Silvery provides the primitives: a Table component with column alignment, VirtualList for constant-memory rendering of massive datasets, useContentRect() for responsive column sizing, and TextInput for real-time filtering.

Key Benefits

  • Table component — Built-in Table with header alignment, column separators, and per-column align ("left", "right", "center"). Column widths auto-size to content when omitted, or accept explicit widths.

  • VirtualList for massive datasets — Render millions of rows with constant memory. Only items within the visible viewport (plus configurable overscan) are mounted. Supports fixed and variable-height items, gap/separator rendering, and imperative scrollToItem().

  • Responsive with useContentRect() — Components query their computed dimensions at render time. Columns auto-size to the terminal width. No width prop drilling.

  • TextInput for search — Combine TextInput with useDeferredValue from React 19 to build responsive filter interfaces. The input stays snappy while expensive filtering runs at lower priority.

  • Inline images — Display thumbnails in table rows using the Image component. Silvery auto-detects Kitty graphics or Sixel protocol support.

Source Code

A process explorer with search filtering, a sortable table, and responsive columns:

tsx
import { useState, useDeferredValue } from "react"
import { Box, Text, Table, TextInput, VirtualList, useContentRect, render, useApp, createTerm } from "silvery"

// Sample data — replace with your own data source
const processes = Array.from({ length: 500 }, (_, i) => ({
  pid: 1000 + i,
  name: ["node", "bun", "vim", "zsh", "git", "ssh", "tmux"][i % 7],
  cpu: (Math.random() * 100).toFixed(1),
  mem: (Math.random() * 8192).toFixed(0),
  status: i % 5 === 0 ? "sleeping" : "running",
}))

function App() {
  const { exit } = useApp()
  const { width, height } = useContentRect()
  const [query, setQuery] = useState("")
  const deferredQuery = useDeferredValue(query)

  // Filter rows against the deferred query so typing stays responsive
  const filtered = processes.filter(
    (p) => p.name.includes(deferredQuery) || String(p.pid).includes(deferredQuery) || p.status.includes(deferredQuery),
  )

  // Responsive column widths
  const nameWidth = Math.max(8, Math.floor(width * 0.3))
  const statusWidth = Math.max(8, Math.floor(width * 0.2))

  return (
    <Box flexDirection="column" width="100%" height="100%">
      <Box paddingX={1} height={1}>
        <Text bold>Filter: </Text>
        <TextInput value={query} onChange={setQuery} placeholder="Search by name, PID, or status..." />
      </Box>

      <Box paddingX={1} flexGrow={1}>
        <Table
          columns={[
            { header: "PID", key: "pid", width: 7, align: "right" },
            { header: "Name", key: "name", width: nameWidth },
            { header: "CPU %", key: "cpu", width: 8, align: "right" },
            { header: "Mem MB", key: "mem", width: 9, align: "right" },
            { header: "Status", key: "status", width: statusWidth },
          ]}
          data={filtered}
        />
      </Box>

      <Box paddingX={1} height={1}>
        <Text dimColor>
          {filtered.length} / {processes.length} processes | q quit
        </Text>
      </Box>
    </Box>
  )
}

using term = createTerm()
await render(<App />, term)

Key Patterns

Responsive Column Widths

useContentRect() gives the terminal width at render time. Columns scale proportionally:

tsx
const { width } = useContentRect()
const nameWidth = Math.max(8, Math.floor(width * 0.3))
const statusWidth = Math.max(8, Math.floor(width * 0.2))

useDeferredValue keeps the TextInput responsive while filtering large datasets:

tsx
const [query, setQuery] = useState("")
const deferredQuery = useDeferredValue(query)
const filtered = data.filter((row) => row.name.includes(deferredQuery))

VirtualList for Large Datasets

For thousands of rows, wrap in a VirtualList to keep memory constant:

tsx
<VirtualList
  items={filtered}
  height={terminalHeight - 4}
  itemHeight={1}
  scrollTo={selected}
  renderItem={(row, index) => <Text inverse={index === selected}>{formatRow(row)}</Text>}
/>

Table with Aligned Columns

The Table component handles header alignment and separators:

tsx
<Table
  columns={[
    { header: "PID", key: "pid", width: 7, align: "right" },
    { header: "Name", key: "name", width: nameWidth },
    { header: "CPU %", key: "cpu", width: 8, align: "right" },
    { header: "Status", key: "status", width: statusWidth },
  ]}
  data={filtered}
/>

Features Used

FeatureUsage
TableColumn alignment and headers
VirtualListConstant-memory rendering of large datasets
useContentRect()Responsive column widths
TextInputReal-time search filter
useDeferredValueNon-blocking filter during typing
useInput()Keyboard navigation

Exercises

  1. Add column sorting — Click a header or press a key to sort by that column
  2. Add row details — Press Enter on a row to expand a detail pane
  3. Add CSV export — Press e to export filtered data to a file
  4. Color-coded status — Red for stopped, green for running, yellow for sleeping

Released under the MIT License.