Silvery vs Bubble Tea
External project claims last verified: 2026-04. Bubble Tea version: 2.x.
Bubble Tea (2020, Charm) is a Go framework for building terminal UIs using The Elm Architecture. Programs are defined by three functions: Init, Update, and View. The Charm ecosystem includes Bubbles (reusable components), Lip Gloss (styling), Huh (form building), and Wish (SSH server for TUI apps). Widely adopted, well-documented, and actively maintained — stable v2.0.0 shipped February 24, 2026 (latest v2.0.2, March 2026) with cell-based rendering, SGR mouse, Kitty keyboard, and more. Bubble Tea is the standard TUI framework for Go.
Silvery is a ground-up React-based terminal UI framework for TypeScript. It combines React's component model with TEA-style state machines (via @silvery/create), CSS flexbox layout (via Flexily), and a rendering pipeline that gives components their dimensions during render. Newer, smaller community, but more built-in features.
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:
- CSS flexbox layout — components auto-size with flex-grow, wrapping, gap, and alignment. Bubble Tea has no layout engine; you join strings manually with Lip Gloss and thread widths/heights yourself.
- Layout-first rendering — components know their size during render via
useBoxRect(). Bubble Tea v2'sView()returns atea.Viewstruct (not just a string), but there is no layout feedback — you must pass sizes down through model state. - React component model — hooks, context, Suspense, third-party React libraries all work. Bubble Tea uses Go structs with manual message routing.
- 45+ built-in components — VirtualList, Table, CommandPalette, TreeView, Toast, Tabs, SplitView, ModalDialog, Image, TextArea, and more. Bubbles provides ~12 components.
- Incremental rendering — cell-level dirty tracking skips unchanged nodes. Bubble Tea v2's cell-based renderer diffs at the cell level too, but re-runs
View()for the full tree on every update. - DOM-style mouse events —
onClick,onWheel,onMouseDownwith hit testing and drag support. Bubble Tea v2 has SGR mouse with typed messages (MouseClickMsg,MouseReleaseMsg, etc.), but events are routed throughUpdatewithout DOM-style bubbling or hit testing. - 84 color schemes with semantic tokens —
$primary,$muted,$borderwith auto-detection. Lip Gloss provides chainable style functions with color downsampling. - Multi-backend test matrix — Termless runs tests across 10+ real terminal parsers (xterm.js, vt100, Ghostty, Kitty, Alacritty, ...).
teatestuses golden file comparison. - Dynamic scrollback — items graduate to terminal history; inline/fullscreen hybrid modes. Bubble Tea v2 supports inline mode but has no scrollback graduation mechanism.
- Fast incremental rendering — cell-level dirty tracking means most of the tree is skipped on interactive updates. Performance is comparable to Ink 7.0 — see benchmarks for details.
Where Bubble Tea is stronger:
- Compiled Go — single static binary, ~1 ms startup, no runtime dependency. Ideal for distributing CLI tools. Silvery requires Node.js or Bun (~50–150 ms startup).
- TEA is native — The Elm Architecture is Bubble Tea's core, not an optional layer. Every program is a pure
(Msg, Model) → (Model, Cmd)function. In Silvery, TEA is available via@silvery/createbut layered on top of React. - Community — the Charm ecosystem (Bubble Tea, Bubbles, Lip Gloss, Huh, Wish, Log) is cohesive, well-documented, and widely adopted. Thousands of Go CLI tools use it. Silvery is newer with a smaller community.
- No React overhead — Bubble Tea has no reconciler, no virtual tree, no hooks lifecycle. The update loop is a simple Go function. For simple TUIs, this directness is an advantage.
- Go ecosystem — any Go library works alongside Bubble Tea. Strong concurrency primitives (goroutines, channels) are built into the language.
What's the same: Both use TEA (Model/Update/View pattern), both support Kitty keyboard protocol (Bubble Tea requests basic key disambiguation and event types by default; does not document all five progressive-enhancement flags), SGR mouse protocol, OSC 52 clipboard, bracketed paste, alternate screen, synchronized output (DEC 2026), and cell-based rendering. Both are pure implementations with no native dependencies in their respective languages.
Feature Matrix
Bubble Tea first, Silvery second. Features marked "core" are built into the framework; "ecosystem" means available via official packages.
Layout & Rendering
| Feature | Bubble Tea v2 | Silvery |
|---|---|---|
| Layout engine | None — manual string joining via Lip Gloss (JoinHorizontal, JoinVertical, Place) | CSS flexbox (Flexily) — flex-grow, wrap, gap, padding, margin, alignment |
| Responsive layout | Manual: pass SetWidth/SetHeight to child models, recalculate on resize | useBoxRect() — dimensions available during render, first pass |
| Rendering approach | Cell-based renderer (v2): View() returns tea.View struct, framework diffs cells | Cell-level buffer with style stacking, 7 dirty flags/node, incremental skip |
| Incremental rendering | Re-runs full View() on every message, then cell-diffs the output | Per-node dirty tracking — unchanged subtrees skip render + diff entirely |
| Scrollable containers | Viewport bubble (manual sizing, scroll offset management) | overflow="scroll" + scrollTo — core framework, handles clipping |
| Sticky headers | Not in core | position="sticky" in scroll containers |
| Dynamic scrollback | Not in core — v2 supports inline mode, but no scrollback graduation mechanism | Items graduate to terminal history; Cmd+F works on graduated content |
| Inline/fullscreen hybrid | v2 supports inline, full-window, or a mix; View.AltScreen is a per-view setting | Inline mode with fullscreen-level performance; fullscreen with scrollback graduation |
| Render targets | Terminal only | Terminal, Canvas 2D, DOM (experimental) |
Interaction
| Feature | Bubble Tea v2 | Silvery |
|---|---|---|
| Mouse support | SGR mouse — typed MouseClickMsg, MouseReleaseMsg, MouseMotionMsg, MouseWheelMsg through Update | SGR mouse with DOM-style events: onClick, onWheel, onMouseDown, hit testing, drag |
| Input handling | All messages go through single Update function | DOM-style bubbling, modal isolation, stopPropagation, input layers |
| Focus system | Manual — manage focused component in model state | Tree-based: scopes, spatial nav (arrow keys), click-to-focus, useFocusWithin |
| Text selection + find | Not in core | Mouse drag, Ctrl+F search, Esc,v keyboard selection |
| Command system | Not in core | Named commands, context-aware keys, parseHotkey("⌘K") |
| Clipboard | OSC 52 (v2) | OSC 52 copyToClipboard/requestClipboard |
| Image rendering | Not in core (ecosystem libraries available) | Core: <Image> — Kitty graphics + Sixel + text fallback |
Components & Framework
| Feature | Bubble Tea v2 | Silvery |
|---|---|---|
| Built-in components | Bubbles: ~12 (spinner, textinput, textarea, viewport, table, list, filepicker, paginator, progress, help, timer, stopwatch) | 45+ core (VirtualList, Table, CommandPalette, TreeView, Toast, Tabs, SplitView, ModalDialog, Image, TextArea, ...) |
| Forms | Huh — form builder with groups, validation, accessibility | Built-in form components (TextInput, SelectList, Checkbox, etc.) |
| Theme system | Lip Gloss: chainable style functions, auto color downsampling (truecolor → 256 → 16) | 84 color schemes, semantic tokens ($primary, $muted), auto-detect terminal scheme |
| TEA state machines | Core — every program is Init/Update/View | Optional via @silvery/create: (action, state) → [state, effects], replay, undo |
| Composition | Embed models in parent, forward messages manually | React JSX nesting + pipe() provider composition |
| SSH server | Wish — serve TUI apps over SSH | None |
| Animation | tea.Tick commands for timer-based animation | useAnimation + easing functions + useAnimatedTransition |
| Resource cleanup | tea.Quit command | using / Disposable — automatic teardown |
Testing
| Feature | Bubble Tea v2 | Silvery |
|---|---|---|
| Test library | teatest — send messages, assert model state, golden file comparison | @silvery/test with Playwright-style locators, press(), buffer assertions |
| Pure function testing | Direct: call Update(msg, model) and assert returned model + cmd | Direct: call TEA reducer and assert state + effects |
| Headless rendering | Not in core | createTerm({ cols, rows }) — no terminal needed |
| Terminal emulator in tests | Not in core | createTermless() via Termless — 10+ backends: xterm.js, vt100, Ghostty, Kitty, Alacritty, and more |
| Render invariant checks | Not in core | SILVERY_STRICT=1 verifies incremental = fresh on every frame |
| Visual snapshots | Golden file comparison (string output) | bufferToHTML(), Playwright capture, .tape recordings → animated GIF, PNG, SVG |
Performance & Distribution
| Aspect | Bubble Tea v2 | Silvery |
|---|---|---|
| Language | Compiled Go | TypeScript (Bun or Node.js) |
| Startup time | ~1 ms (compiled binary) | ~50–150 ms (JS runtime initialization) |
| Distribution | Single static binary — go build, cross-compile, no dependencies | Requires Node.js/Bun runtime; bundle with bun build or ship as npm package |
| Interactive update speed | Fast (compiled Go, cell diff) | Fast incremental rendering — cell-level dirty tracking skips unchanged subtrees entirely. Performance is comparable to Ink 7.0 — see benchmarks for details |
| Output efficiency | Cell-based diff (v2) | 10–20× less output — cell-level diff + relative cursor addressing |
| Memory | Go GC with low pause times | Normal JS GC; graduated scrollback frees React tree |
| Native dependencies | None (compiled Go) | None (pure TypeScript) |
| Type safety | Go's type system (interfaces, generics since 1.18) | TypeScript strict mode (generics, discriminated unions, branded types) |
Key Differences Explained
Layout
This is the most significant architectural difference.
Bubble Tea has no layout engine. You build layouts by joining strings with Lip Gloss:
// Bubble Tea + Lip Gloss: manual layout via string joining
left := lipgloss.NewStyle().Width(30).Render(sidebar.View())
right := lipgloss.NewStyle().Width(50).Render(content.View())
layout := lipgloss.JoinHorizontal(lipgloss.Top, left, right)You must calculate widths and heights yourself, pass them down to child models with SetWidth/SetHeight, and recalculate on terminal resize. There is no automatic flex-grow, no wrapping, no gap, no alignment that responds to available space.
Silvery has CSS flexbox via the Flexily layout engine:
// Silvery: CSS flexbox layout
<Box flexDirection="row" gap={1}>
<Box width={30}>
<Sidebar />
</Box>
<Box flexGrow={1}>
<Content />
</Box>
</Box>Components can read their computed dimensions during render via useBoxRect(). No manual size threading, no resize handlers calculating widths. The layout engine handles flex-grow, flex-shrink, wrapping, padding, margin, borders, gap, and alignment automatically.
For simple UIs (a list, a form, a spinner), this difference barely matters. For complex UIs (multi-pane dashboards, kanban boards, text editors with sidebars), it's substantial.
TEA: Same Pattern, Different Surfaces
Both frameworks build on The Elm Architecture, but they expose it differently.
Bubble Tea is TEA all the way down. Every program is a Model struct with Init, Update, and View methods. Composition means embedding models and forwarding messages:
// Bubble Tea: pure TEA
type Model struct {
spinner spinner.Model
list list.Model
quitting bool
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
if msg.String() == "q" {
m.quitting = true
return m, tea.Quit
}
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
func (m Model) View() tea.View {
return m.list.View()
}Silvery uses React for the component tree and rendering, with TEA available as an optional layer via @silvery/create for state management:
// Silvery: React components, optional TEA for state
function App() {
const { width } = useBoxRect()
const [items] = useState(loadItems)
return (
<Box flexDirection="column">
<SelectList items={items} />
</Box>
)
}// Silvery TEA (optional, via @silvery/create):
// Pure (action, state) => [state, effects] for testable state machines
const [state, dispatch] = useTea(reducer, initialState)The practical difference: in Bubble Tea, you manage all state transitions and message routing yourself — this is explicit and testable (you can call Update directly in tests). In Silvery, React handles the component tree, reconciliation, and rendering — you only use TEA where you want explicit state machine semantics (complex interactions, undo/redo, replay).
Composition
Bubble Tea composes models by embedding them in parent models and manually routing messages. This is explicit but can become verbose with deeply nested UIs:
// Bubble Tea: manual message routing
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
// Route to each child
m.header, cmd = m.header.Update(msg)
cmds = append(cmds, cmd)
m.sidebar, cmd = m.sidebar.Update(msg)
cmds = append(cmds, cmd)
m.content, cmd = m.content.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}Silvery uses React's component tree -- composition is JSX nesting:
// Silvery: React composition
function App() {
return (
<Box flexDirection="row">
<Sidebar />
<Box flexDirection="column" flexGrow={1}>
<Header />
<Content />
</Box>
</Box>
)
}Styling
Lip Gloss provides chainable style functions:
style := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("205")).
Background(lipgloss.Color("236")).
Padding(0, 1)
rendered := style.Render("Hello")Lip Gloss v2 integrates with Bubble Tea for automatic color downsampling -- truecolor styles degrade gracefully on 256-color or 16-color terminals.
Silvery uses semantic theme tokens with 84 color schemes:
<Text color="$primary">Important</Text>
<Text color="$muted">Secondary</Text>
<Box borderStyle="round" borderColor="$border">
<Text>Framed content</Text>
</Box>Silvery auto-detects the terminal's color scheme and selects an appropriate palette. Both approaches work well; Lip Gloss gives more explicit control, Silvery provides more consistency across terminals.
Testing
Bubble Tea has teatest for integration-style testing:
func TestApp(t *testing.T) {
m := NewModel()
tm := teatest.NewTestModel(t, m,
teatest.WithInitialTermSize(80, 24),
)
tm.Send(tea.KeyPressMsg{Runes: []rune("j")})
out := tm.FinalOutput(t)
teatest.RequireEqualOutput(t, out) // golden file comparison
}Testing is straightforward because TEA models are pure functions -- you can call Update directly with any message and assert the resulting model state.
Silvery has two testing layers:
// Fast: headless renderer with auto-locators
import { createRenderer } from "@silvery/test"
using app = await createRenderer(<App />, { cols: 80, rows: 24 })
expect(app).toContainText("Hello")
await app.press("j")
expect(app.getByRole("listitem", { selected: true })).toHaveTextContent("Item 2")// Full: terminal emulator (Termless) for ANSI verification
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")Silvery's Termless runs a real terminal emulator in-process -- you can verify ANSI escape sequences, resolved RGB colors per cell, bold/italic/underline attributes, cursor position, and scrollback content. Bubble Tea's golden file approach compares rendered string output, which works well for catching regressions but doesn't validate terminal protocol correctness.
Performance
Bubble Tea compiles to a native binary. Startup is near-instant (~1 ms). Rendering is fast because Go is compiled and garbage-collected with low pause times. The View function produces a string on every update; Bubble Tea v2's cell-based renderer then diffs at the cell level.
Silvery runs on a JavaScript runtime (Bun or Node.js). Startup includes runtime initialization (~50–150 ms). Once running, Silvery's incremental rendering skips unchanged nodes entirely — a typical interactive update (cursor move in a 1000-node tree) takes ~169 us. The 5-phase pipeline (measure, layout, content, diff, output) has overhead for full re-renders, but interactive updates are sub-millisecond because most of the tree is skipped.
For CLIs that start, do one thing, and exit, Go's startup advantage is real — 1 ms vs 50–150 ms matters when users run the command hundreds of times a day. For interactive TUIs that run for minutes or hours, runtime startup is irrelevant and per-update performance matters more. Silvery's cell-level dirty tracking skips unchanged subtrees entirely rather than re-running the full view function. Performance is comparable to Ink 7.0 — see benchmarks for details.
Ecosystem
The Charm ecosystem is cohesive and well-designed:
| Package | What |
|---|---|
| Bubble Tea | Core framework |
| Bubbles | Components (spinner, textinput, textarea, viewport, table, list, filepicker, paginator, progress, help, timer, stopwatch) |
| Lip Gloss | Styling and layout |
| Huh | Forms and prompts |
| Wish | SSH server for TUI apps |
| Log | Styled logging |
Silvery's ecosystem is smaller but more integrated:
| Package | What |
|---|---|
silvery | Core renderer + 45+ components |
@silvery/create | TEA state machines |
@silvery/test | Testing (headless + Termless emulator) |
@silvery/theme | 84 color schemes, semantic tokens |
@silvery/commands | Command system with keybindings |
@silvery/ansi | Terminal primitives, styling, detection |
Bubble Tea benefits from the broader Go ecosystem -- any Go library works alongside it. Silvery benefits from the React/npm ecosystem -- any React pattern (hooks, context, suspense) works inside it.
When to Choose What
Both are excellent tools. The right choice depends on what you're building and in which language.
Choose Bubble Tea when:
- Your project is in Go — Bubble Tea is the standard TUI framework for Go, and mixing languages adds complexity
- You want a single binary — Go compiles to a static binary with no runtime dependencies, ideal for distributing CLI tools (
brew install,go install, download from GitHub releases) - You prefer pure TEA — if you want the Elm Architecture without React's abstraction layer, Bubble Tea is TEA in its most direct form. Every program is a testable
(Msg, Model) → (Model, Cmd)function - Simple-to-medium complexity UIs — for CLIs, dashboards, and tools where manual layout is manageable, Bubble Tea's explicit approach works well
- Startup time matters — compiled Go starts in ~1 ms; JavaScript runtimes need 50–150 ms. For commands users run hundreds of times a day, this adds up
- SSH-served TUIs — Wish lets you serve Bubble Tea apps over SSH with no client-side installation
Choose Silvery when:
- Your project is in TypeScript/JavaScript — using a Go framework from a Node.js project means running a subprocess or rewriting in Go
- Complex layouts — CSS flexbox handles multi-pane, responsive UIs that would require substantial manual calculation in Bubble Tea
- React ecosystem — hooks, context, component composition, third-party React libraries all work directly
- Rich built-in components — 45+ components (VirtualList, TextArea, Table, CommandPalette, ModalDialog) without assembling individual packages
- Mouse interaction — DOM-style event handling (
onClick,onWheel, drag) with hit testing - Interactive update performance — per-node dirty tracking for sub-millisecond updates in large trees
- Terminal protocol depth — image rendering, extended underlines, OSC sequences, terminal capability detection via terminfo.dev
- Testing terminal output — Termless gives you a real terminal emulator in-process with 10+ backends for verifying ANSI output, not just string snapshots
- Inline/fullscreen hybrid — inline mode with fullscreen performance, or fullscreen with scrollback graduation