Skip to content

Silvery vs Textual

External project claims last verified: 2026-04. Textual version: 8.2.3.

Textual (2021, Textualize) is the leading TUI framework for Python. Created by Will McGugan — author of Rich, the library that made beautiful terminal output mainstream — Textual brings CSS-like styling (TCSS), a reactive widget tree, and asyncio event handling to Python. Large widget library (dozens of built-in widgets), active development, strong documentation, and a genuine web deployment story via Textual Web. Excellent engineering from a team that deeply understands terminal rendering.

Silvery (2025) is a React-based terminal UI framework for TypeScript. Same broad goal — bring web-style development patterns to the terminal — but different language, different rendering architecture, and different trade-offs.

Silvery grew out of building a complex terminal app where components needed to know their size during render, updates needed to be fast, and scroll containers, mouse events, focus scopes, and Playwright-style testing needed to just work. Three principles emerged: take the best from the web, stay true to the terminal, and raise the bar for developer ergonomics, architecture composability, and performance.

Highlights

The biggest differences at a glance:

  • React ecosystem — JavaScript/TypeScript, npm, hooks, context, Suspense, concurrent mode. Textual is Python with its own widget lifecycle and reactive attributes.
  • Layout-first rendering — components know their size during render via useBoxRect(). Textual widgets query self.size after layout, similar to web components.
  • Cell-level incremental rendering — per-node dirty tracking (7 flags/node), cell-level buffer diff. Textual uses Rich's rendering pipeline with careful caching.
  • Multi-target — terminal, Canvas 2D, DOM (experimental). Textual has a mature web target via Textual Web (serve TUI in browser).
  • Fast incremental rendering — cell-level dirty tracking with per-node dirty flags. Performance is comparable to Ink 7.0 — see benchmarks for details. No direct Textual benchmarks; in practice, all three frameworks are fast enough for most TUI apps.
  • Termless testingTermless runs tests across 10+ real terminal parsers (xterm.js, vt100, Ghostty, Kitty, Alacritty, ...). Verify resolved RGB colors per cell, not just widget state.
  • Ink compatibility layer — 99% of Ink's tests pass on silvery's compat layer. If you're in the JS ecosystem and have Ink code, it runs with minimal changes.
  • Blurred inline/fullscreen boundaryinline mode gets cell-level incremental rendering and dynamic scrollback graduation; fullscreen mode gets app-managed scrollback history.

Where Textual is stronger:

  • Python ecosystem — huge community, data science libraries, automation tools. If your project is Python, Textual is the natural choice.
  • CSS-like styling — TCSS (a CSS subset) with selectors, pseudo-classes (:focus, :hover, :disabled), hot-reload during development, and separation of concerns. Silvery uses inline props.
  • Web deploymenttextual serve myapp.py serves any Textual app in a browser with no client installation. Silvery's multi-target rendering is experimental.
  • Grid layout — TCSS supports CSS Grid-style layouts alongside flexbox and dock. Silvery is flexbox-only.
  • Documentation — Textual's docs site is comprehensive and well-organized. Silvery's docs are growing.
  • Hot reload — CSS changes apply instantly during development without restarting.
  • Mature widget library — dozens of built-in widgets with consistent styling, including DataTable, Markdown viewer, DirectoryTree, Sparkline, and RichLog.

What's the same: component trees, flexbox-capable layout, mouse support, scrollable containers, focus system, rich text styling, reactive state management, headless testing, clipboard, hyperlinks, pure interpreted language (no native deps). Both aim to make terminal UIs feel like web development.

At a Glance

AspectTextualSilvery
LanguagePythonTypeScript
ArchitectureWidget tree + TCSS + reactive attributesReact component tree + CSS flexbox
StylingTCSS (CSS subset in .tcss files)Semantic theme tokens + inline props
LayoutDock, grid, horizontal, verticalCSS flexbox (Flexily engine)
ComponentsDozens of built-in widgets (DataTable, Input, Button, Select, Tree, TextArea, Markdown, RichLog, Sparkline, Tabs, CommandPalette, etc.)45+ built-in (VirtualList, TextArea, SelectList, Table, CommandPalette, ModalDialog, Tabs, TreeView, Toast, Image, SplitView, etc.)
TestingPilot mode (async, simulated events)@silvery/test (headless renderer, locators) + Termless (10+ terminal backends)
Mouse supportFull (click, scroll, hover)SGR protocol with DOM-style events
KeyboardStandard terminal inputKitty keyboard protocol (all 5 flags)
Focus systemTab-based with focusable widgetsTree-based with scopes, spatial navigation
ScrollingBuilt-in per-widget, ScrollableContaineroverflow="scroll", VirtualList
ClipboardBuilt-in App.copy_to_clipboard() APIOSC 52 (works over SSH)
Image renderingNone built-inKitty graphics + Sixel with auto-detect
Web targetTextual Web (serve TUI in browser)Experimental (Canvas 2D, DOM)
Theme systemTCSS variables + built-in themes84 color schemes, semantic tokens ($primary, $muted), auto-detect
RuntimeCPython / PyPyNode.js / Bun
Native depsNoneNone
CommunityLarge (Python TUI standard)Newer, smaller community

CSS: TCSS vs Flexbox Props

Both frameworks bring CSS concepts to the terminal, but in different ways.

Textual's TCSS

Textual has its own CSS dialect (TCSS) written in .tcss files. It supports selectors, pseudo-classes, and a subset of CSS properties adapted for terminals:

python
# Python widget
class Sidebar(Widget):
    pass

class MainApp(App):
    CSS = """
    Sidebar {
        dock: left;
        width: 30;
        background: $surface;
    }

    Sidebar:focus-within {
        border: tall $accent;
    }

    #content {
        height: 1fr;
    }
    """

    def compose(self):
        yield Sidebar()
        yield Container(id="content")

TCSS supports type selectors, ID selectors (#id), class selectors (.class), pseudo-classes (:focus, :hover, :disabled), and combinators. Properties include dock, width, height, margin, padding, background, color, border, display, visibility, overflow, and layout-specific properties like grid-size-columns and grid-size-rows.

The separation of styling from code is a strength -- you can restyle widgets without changing Python code, and TCSS hot-reloads during development.

Silvery's Flexbox Props

Silvery uses React props for styling, with semantic theme tokens:

tsx
function App() {
  return (
    <Box flexDirection="row">
      <Box width={30} borderStyle="round" borderColor="$border">
        <Sidebar />
      </Box>
      <Box flexGrow={1} flexDirection="column">
        <Header />
        <Content />
      </Box>
    </Box>
  )
}

Layout is CSS flexbox via the Flexily engine -- flexDirection, flexGrow, flexShrink, flexWrap, gap, alignItems, justifyContent, padding, margin, and border all work as they do in browser CSS.

Silvery does not have external stylesheet files. Styling is inline (props) or via theme tokens ($primary, $muted, $border). This is closer to React Native or Tailwind than traditional CSS.

Trade-off: TCSS gives you selector-based styling with hot-reload and separation of concerns. Silvery's inline props are more explicit and co-located with components but lack the cascading and selector power of TCSS.

Layout

Textual

Textual offers several layout systems:

  • Vertical -- stack widgets top-to-bottom (default)
  • Horizontal -- arrange widgets left-to-right
  • Grid -- CSS Grid-style rows and columns
  • Dock -- pin widgets to screen edges (top, bottom, left, right)
python
class MyApp(App):
    CSS = """
    #sidebar { dock: left; width: 30; }
    #main { height: 1fr; }
    #footer { dock: bottom; height: 3; }
    """

    def compose(self):
        yield Sidebar(id="sidebar")
        yield Container(id="main")
        yield Footer(id="footer")

The fr (fraction) unit distributes remaining space proportionally, similar to CSS Grid. Dock pulls widgets to edges before remaining space is calculated.

Silvery

Silvery uses CSS flexbox exclusively:

tsx
<Box flexDirection="row" height="100%">
  <Box width={30}>
    <Sidebar />
  </Box>
  <Box flexGrow={1} flexDirection="column">
    <Box flexGrow={1}>
      <Content />
    </Box>
    <Box height={3}>
      <Footer />
    </Box>
  </Box>
</Box>

No grid layout, no dock. Everything is flexbox. This is limiting compared to Textual's layout variety, but flexbox handles most TUI layouts well -- and if you know CSS flexbox from web development, there is nothing new to learn.

Silvery's key layout advantage is useBoxRect() -- components know their dimensions during render, not after. Textual widgets can query their size via self.size but this is set during the layout phase, similar to how web components work.

Widget Libraries

Both frameworks have substantial widget libraries.

Textual Widgets

Textual ships dozens of built-in widgets. A representative sample:

WidgetWhat
ButtonClickable button with variants
InputSingle-line text input
TextAreaMulti-line text editor with syntax highlighting
DataTableSortable, scrollable data grid
TreeExpandable tree view
Select / SelectionListDropdown and multi-select
Markdown / MarkdownViewerMarkdown rendering
RichLogScrollable log output with Rich formatting
Tabs / TabbedContentTabbed container
SparklineInline data visualization
ProgressBar / LoadingIndicatorProgress feedback
Header / FooterApp chrome
ListView / OptionListScrollable item lists
DirectoryTreeFile browser
Switch / Checkbox / RadioButtonToggle controls
CommandPaletteBuilt-in fuzzy command search (Ctrl+P)
Toast / notificationsBuilt-in notify() API

Silvery Components

Silvery ships 45+ components:

ComponentWhat
TextInputSingle-line with readline (Ctrl+A/E/K/U, Alt+B/F)
TextAreaMulti-line with word wrap, scroll, undo/redo
SelectListInteractive list with j/k navigation
VirtualListVirtualized scrolling for large datasets
TableTabular data display
TreeViewExpandable tree
TabsTabbed navigation
CommandPaletteFuzzy command search (VS Code-style)
ModalDialogModal with focus trapping
ToastNotification popups
Spinner / ProgressBarProgress feedback
ImageKitty graphics / Sixel with auto-detect
SplitViewResizable split panes
ConsoleComposable console output
LinkOSC 8 clickable hyperlinks

Textual's widget library is more mature, with some unique components (DataTable with sorting, Markdown viewer, DirectoryTree, Sparkline, RichLog). Both frameworks have a built-in command palette and toast/notification system. Silvery has some unique components of its own (Image, SplitView) and all components integrate with the framework's focus system and input layering.

Reactive State

Textual

Textual uses reactive attributes -- decorated properties that automatically trigger UI updates:

python
class Counter(Widget):
    count = reactive(0)

    def watch_count(self, new_value: int) -> None:
        self.query_one("#display").update(str(new_value))

    def on_button_pressed(self, event: Button.Pressed) -> None:
        self.count += 1

The reactive descriptor + watch_* pattern is similar to Vue's watchers. State changes automatically invalidate the widget for re-rendering.

Silvery

Silvery uses React's standard state model:

tsx
function Counter() {
  const [count, setCount] = useState(0)
  return (
    <Box>
      <Text>{count}</Text>
      <Button onPress={() => setCount((c) => c + 1)}>+1</Button>
    </Box>
  )
}

React hooks (useState, useReducer, useContext, useMemo, useCallback) work as expected. For complex state, @silvery/create provides TEA-style pure reducers with serializable actions.

Both approaches work well. Textual's reactive attributes are more implicit (mutation triggers updates). React's hooks are more explicit (state updates must go through setter functions).

Testing

Textual Pilot

Textual's pilot mode runs the app headlessly for testing:

python
async def test_counter():
    app = CounterApp()
    async with app.run_test() as pilot:
        await pilot.press("up")
        assert app.query_one("#count").renderable == "1"
        await pilot.click("#reset")
        assert app.query_one("#count").renderable == "0"

You can press keys, click widgets by CSS selector, and assert widget state. The app runs with a simulated terminal.

Silvery Testing

Silvery has two testing layers:

tsx
// Fast: headless renderer with Playwright-style locators
using app = await createRenderer(<App />, { cols: 80, rows: 24 })
expect(app).toContainText("Count: 0")
await app.press("up")
expect(app).toContainText("Count: 1")
tsx
// Full: Termless — in-process terminal emulator with 10+ backends
import { createTermless } from "@silvery/test"
import "@termless/test/matchers"

using term = createTermless({ cols: 80, rows: 24 })
const handle = await run(<App />, term)

expect(term.screen).toContainText("Dashboard")
expect(term.cell(0, 10)).toBeBold()
expect(term.row(0)).toHaveFg({ r: 255, g: 255, b: 255 })
await handle.press("j") // Navigate down
expect(term.scrollback).toContainText("Previous item")

Termless runs a real terminal emulator in-process — xterm.js, vt100, libvterm, Ghostty, Kitty, Alacritty, WezTerm, and more. Matrix-test across real parsers to verify resolved RGB colors per cell, bold/italic/underline attributes, cursor position, and scrollback content — not just widget state. SILVERY_STRICT=1 verifies incremental rendering matches fresh rendering on every frame.

Terminal Protocol Support

This is where the frameworks diverge significantly.

FeatureTextualSilvery
True colorYesYes
256 colorYesYes
Color downsamplingYes (automatic)Yes (via @silvery/ansi)
Mouse supportYes (click, scroll, hover)Yes (SGR protocol, DOM-style events, drag)
Kitty keyboardNoAll 5 flags (disambiguate, events, alternate, all keys, text)
Key event typesPressPress, repeat, release
Synchronized outputNoDEC mode 2026 (flicker-free in tmux/Zellij)
Extended underlinesCurly, dotted, dashedFull ISO 8613-6 (single, double, curly, dotted, dashed + color)
ClipboardBuilt-in App.copy_to_clipboard()OSC 52 (works over SSH)
HyperlinksDedicated Link widgetOSC 8 with <Link> component
ImagesNoKitty graphics + Sixel with auto-detect
Window titleYesOSC 0/2
Terminal queriesLimitedDA1/DA2/DA3, XTVERSION, CPR, pixel dimensions
Bracketed pasteYesYes (usePaste hook)
Scroll regionsNoDECSTBM
Cursor stylesLimitedFull DECSCUSR (block, underline, bar, blinking)

Silvery's terminal protocol coverage is broader, particularly for Kitty keyboard (important for distinguishing Ctrl+I from Tab, Ctrl+M from Enter), synchronized output (eliminates flicker in terminal multiplexers), and image rendering.

Textual compensates with its web target -- Textual Web can serve any Textual app in a browser, which is a different kind of cross-platform story.

Performance

Python and TypeScript are both interpreted languages, so neither has Go or Rust-level raw speed. In practice, terminal rendering is rarely the bottleneck -- network I/O, file access, and computation dominate.

Textual uses asyncio and careful caching. Widget rendering is optimized with Rich's rendering pipeline. Large DataTables use virtual scrolling for 1000+ rows.

Silvery uses incremental rendering with per-node dirty tracking. Cell-level buffer diff means only changed characters generate output. Performance is comparable to Ink 7.0 — see benchmarks for details. We have not directly benchmarked against Textual (different language runtimes make apples-to-apples comparison difficult).

For most applications, both are fast enough. If you're building an app with thousands of rapidly updating nodes, Silvery's incremental approach has an advantage. If you're building a data dashboard that updates every few seconds, both handle it comfortably.

Web Target

Textual Web can serve any Textual app as a web application -- users access it via a browser with no installation. This is a genuine differentiator for deployment scenarios (internal tools, dashboards, remote access):

bash
textual serve myapp.py

Silvery has experimental Canvas 2D and DOM render targets. These are not production-ready but are on the roadmap. The architecture supports multi-target rendering because the layout and rendering pipeline is decoupled from terminal output.

When to Choose What

Both are good tools. The right choice depends primarily on your language ecosystem.

Choose Textual when:

  • Your project is in Python -- Textual integrates naturally with Python data science, web, and automation ecosystems
  • You want CSS-like styling -- separate stylesheet files with selectors, pseudo-classes, and hot-reload during development
  • Web deployment matters -- Textual Web serves TUI apps in the browser with no client installation
  • Rich widget library -- dozens of built-in widgets with consistent styling and behavior
  • Data-oriented apps -- DataTable, Sparkline, RichLog, and Rich formatting are well-suited for dashboards and data tools
  • Grid layout -- TCSS supports CSS Grid-style layouts alongside flexbox and dock

Choose Silvery when:

  • Your project is in TypeScript/JavaScript -- React components, npm packages, TypeScript type safety
  • Complex interactive UIs -- kanban boards, text editors, multi-pane dashboards where layout-aware rendering matters
  • React ecosystem -- hooks, context, component composition, Suspense, and the full React mental model
  • Terminal protocol depth -- Kitty keyboard, synchronized output, image rendering, clipboard over SSH, terminal capability detection
  • Testing terminal output -- Termless verifies actual ANSI sequences and resolved colors across 10+ real terminal parsers, not just widget state
  • TEA state machines -- @silvery/create provides pure (action, state) -> [state, effects] reducers alongside React
  • Input isolation -- InputLayerProvider with DOM-style bubbling and stopPropagation for modal dialogs and layered UIs
  • Dynamic scrollback -- inline mode with cell-level incremental rendering and scrollback graduation; fullscreen mode with app-managed history