Skip to content

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's View() returns a tea.View struct (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 componentsVirtualList, 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 eventsonClick, onWheel, onMouseDown with hit testing and drag support. Bubble Tea v2 has SGR mouse with typed messages (MouseClickMsg, MouseReleaseMsg, etc.), but events are routed through Update without DOM-style bubbling or hit testing.
  • 84 color schemes with semantic tokens$primary, $muted, $border with auto-detection. Lip Gloss provides chainable style functions with color downsampling.
  • Multi-backend test matrixTermless runs tests across 10+ real terminal parsers (xterm.js, vt100, Ghostty, Kitty, Alacritty, ...). teatest uses 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/create but 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

FeatureBubble Tea v2Silvery
Layout engineNone — manual string joining via Lip Gloss (JoinHorizontal, JoinVertical, Place)CSS flexbox (Flexily) — flex-grow, wrap, gap, padding, margin, alignment
Responsive layoutManual: pass SetWidth/SetHeight to child models, recalculate on resizeuseBoxRect() — dimensions available during render, first pass
Rendering approachCell-based renderer (v2): View() returns tea.View struct, framework diffs cellsCell-level buffer with style stacking, 7 dirty flags/node, incremental skip
Incremental renderingRe-runs full View() on every message, then cell-diffs the outputPer-node dirty tracking — unchanged subtrees skip render + diff entirely
Scrollable containersViewport bubble (manual sizing, scroll offset management)overflow="scroll" + scrollTo — core framework, handles clipping
Sticky headersNot in coreposition="sticky" in scroll containers
Dynamic scrollbackNot in core — v2 supports inline mode, but no scrollback graduation mechanismItems graduate to terminal history; Cmd+F works on graduated content
Inline/fullscreen hybridv2 supports inline, full-window, or a mix; View.AltScreen is a per-view settingInline mode with fullscreen-level performance; fullscreen with scrollback graduation
Render targetsTerminal onlyTerminal, Canvas 2D, DOM (experimental)

Interaction

FeatureBubble Tea v2Silvery
Mouse supportSGR mouse — typed MouseClickMsg, MouseReleaseMsg, MouseMotionMsg, MouseWheelMsg through UpdateSGR mouse with DOM-style events: onClick, onWheel, onMouseDown, hit testing, drag
Input handlingAll messages go through single Update functionDOM-style bubbling, modal isolation, stopPropagation, input layers
Focus systemManual — manage focused component in model stateTree-based: scopes, spatial nav (arrow keys), click-to-focus, useFocusWithin
Text selection + findNot in coreMouse drag, Ctrl+F search, Esc,v keyboard selection
Command systemNot in coreNamed commands, context-aware keys, parseHotkey("⌘K")
ClipboardOSC 52 (v2)OSC 52 copyToClipboard/requestClipboard
Image renderingNot in core (ecosystem libraries available)Core: <Image> — Kitty graphics + Sixel + text fallback

Components & Framework

FeatureBubble Tea v2Silvery
Built-in componentsBubbles: ~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, ...)
FormsHuh — form builder with groups, validation, accessibilityBuilt-in form components (TextInput, SelectList, Checkbox, etc.)
Theme systemLip Gloss: chainable style functions, auto color downsampling (truecolor → 256 → 16)84 color schemes, semantic tokens ($primary, $muted), auto-detect terminal scheme
TEA state machinesCore — every program is Init/Update/ViewOptional via @silvery/create: (action, state) → [state, effects], replay, undo
CompositionEmbed models in parent, forward messages manuallyReact JSX nesting + pipe() provider composition
SSH serverWish — serve TUI apps over SSHNone
Animationtea.Tick commands for timer-based animationuseAnimation + easing functions + useAnimatedTransition
Resource cleanuptea.Quit commandusing / Disposable — automatic teardown

Testing

FeatureBubble Tea v2Silvery
Test libraryteatest — send messages, assert model state, golden file comparison@silvery/test with Playwright-style locators, press(), buffer assertions
Pure function testingDirect: call Update(msg, model) and assert returned model + cmdDirect: call TEA reducer and assert state + effects
Headless renderingNot in corecreateTerm({ cols, rows }) — no terminal needed
Terminal emulator in testsNot in corecreateTermless() via Termless — 10+ backends: xterm.js, vt100, Ghostty, Kitty, Alacritty, and more
Render invariant checksNot in coreSILVERY_STRICT=1 verifies incremental = fresh on every frame
Visual snapshotsGolden file comparison (string output)bufferToHTML(), Playwright capture, .tape recordings → animated GIF, PNG, SVG

Performance & Distribution

AspectBubble Tea v2Silvery
LanguageCompiled GoTypeScript (Bun or Node.js)
Startup time~1 ms (compiled binary)~50–150 ms (JS runtime initialization)
DistributionSingle static binary — go build, cross-compile, no dependenciesRequires Node.js/Bun runtime; bundle with bun build or ship as npm package
Interactive update speedFast (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 efficiencyCell-based diff (v2)10–20× less output — cell-level diff + relative cursor addressing
MemoryGo GC with low pause timesNormal JS GC; graduated scrollback frees React tree
Native dependenciesNone (compiled Go)None (pure TypeScript)
Type safetyGo'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:

go
// 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:

tsx
// 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:

go
// 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:

tsx
// Silvery: React components, optional TEA for state
function App() {
  const { width } = useBoxRect()
  const [items] = useState(loadItems)

  return (
    <Box flexDirection="column">
      <SelectList items={items} />
    </Box>
  )
}
tsx
// 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:

go
// 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:

tsx
// 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:

go
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:

tsx
<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:

go
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:

tsx
// 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")
tsx
// 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:

PackageWhat
Bubble TeaCore framework
BubblesComponents (spinner, textinput, textarea, viewport, table, list, filepicker, paginator, progress, help, timer, stopwatch)
Lip GlossStyling and layout
HuhForms and prompts
WishSSH server for TUI apps
LogStyled logging

Silvery's ecosystem is smaller but more integrated:

PackageWhat
silveryCore renderer + 45+ components
@silvery/createTEA state machines
@silvery/testTesting (headless + Termless emulator)
@silvery/theme84 color schemes, semantic tokens
@silvery/commandsCommand system with keybindings
@silvery/ansiTerminal 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 TUIsWish 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 outputTermless gives you a real terminal emulator in-process with 10+ backends for verifying ANSI output, not just string snapshots
  • Inline/fullscreen hybridinline mode with fullscreen performance, or fullscreen with scrollback graduation