Skip to content

Silvery vs Textual

Information about Textual as of March 2026.

Why This Page Exists

Textual is the leading TUI framework for Python, built by the creator of Rich. Both Textual and Silvery aim to bring web-style development patterns to the terminal -- CSS-like styling, component trees, reactive state. They arrive at similar goals from different languages and different architectures.

This page compares them honestly so you can pick the right one.

The Two Projects

Textual (2021, Textualize) is a Python framework for building terminal UIs. Created by Will McGuigan (author of Rich), it uses a widget tree with CSS-like styling (TCSS), reactive attributes, and asyncio for event handling. Large widget library, active development, and a growing community. Apps can run in both terminal and web browser via Textual Web. See the documentation for the full API reference.

Silvery (2025) is a React-based terminal UI framework for TypeScript. It uses React's component model with CSS flexbox layout (via Flexily), incremental rendering, and comprehensive terminal protocol support. Newer and smaller community, but more built-in terminal features.

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)
Components40+ built-in (DataTable, Input, Button, Select, Tree, TextArea, Markdown, RichLog, Sparkline, Tabs, OptionList, etc.)30+ 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 (terminal emulator)
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
ClipboardPython-level (pyperclip etc.)OSC 52 (works over SSH)
Image renderingNone built-inKitty graphics + Sixel with auto-detect
Web targetTextual Web (serve TUI in browser)Experimental (Canvas 2D, DOM)
RuntimeCPython / PyPyNode.js / Bun / Deno
Native depsNoneNone
CommunityLarge (Python TUI standard)New

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 useContentRect() -- 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 40+ 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

Silvery Components

Silvery ships 30+ 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 larger and more mature. Silvery's is smaller but includes some unique components (CommandPalette, 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)
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, so you can verify resolved RGB colors per cell, bold/italic/underline attributes, cursor position, and scrollback content -- not just widget state. This is unique to Silvery and matters when testing terminal protocol correctness.

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)
ClipboardVia system clipboardOSC 52 (works over SSH)
HyperlinksYes (Rich links)OSC 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. A typical interactive update (cursor move in a 1000-node tree) takes ~169 us because unchanged nodes are skipped entirely. The cell-level buffer diff means only changed characters generate output.

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

Choose Textual when:

  • Your project is in Python -- Textual integrates naturally with Python data science, web, and automation ecosystems
  • You want TCSS 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 -- 40+ 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 via terminfo.dev
  • Testing terminal output -- Termless verifies actual ANSI sequences and resolved colors, 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