Terminal UI: BubbleTea (Go) vs Ratatui (Rust)
Elm-style (Go) vs immediate-mode (Rust) TUI frameworks quickview
Two strong options for building terminal user interfaces today are BubbleTea (Go) and Ratatui (Rust). One gives you an opinionated, Elm-style framework; the other a flexible, immediate-mode library.
This post sums up what each is, shows a minimal example for both, and suggests when to pick which. And yes, also gives some useful links.

Crush UI (screenshot above) is implemented using BubbleTea framework.
What is BubbleTea?
BubbleTea is a Go framework for TUIs based on The Elm Architecture. You describe your app with a model (state) and three pieces: Init (initial command), Update (handle messages, return new model and optional command), and View (render the UI as a string). The framework runs the event loop, turns keypresses and I/O into messages, and redraws when the model changes. So: what is BubbleTea? In short, it’s the fun, stateful way to build terminal apps in Go, with a single source of truth and predictable updates.
BubbleTea is production-ready (v1.x), has tens of thousands of GitHub stars and thousands of known importers, and works inline, full-screen, or mixed. You typically pair it with Bubbles for components (inputs, viewports, spinners) and Lip Gloss for styling. If you’re building a Go TUI, a solid first step is to follow common Go project structure so your cmd/ and packages stay clear as the app grows.
A minimal BubbleTea program looks like this: a model, Init, Update (handling key messages), and View returning a string. The runtime does the rest.
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
type model struct {
choices []string
cursor int
selected map[int]struct{}
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "up", "k":
if m.cursor > 0 { m.cursor-- }
case "down", "j":
if m.cursor < len(m.choices)-1 { m.cursor++ }
case "enter", " ":
if _, ok := m.selected[m.cursor]; ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
return m, nil
}
func (m model) View() string {
s := "What should we buy?\n\n"
for i, choice := range m.choices {
cursor := " "
if m.cursor == i { cursor = ">" }
checked := " "
if _, ok := m.selected[i]; ok { checked = "x" }
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
return s + "\nPress q to quit.\n"
}
func main() {
if _, err := tea.NewProgram(model{
choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
selected: make(map[int]struct{}),
}).Run(); err != nil {
fmt.Printf("error: %v", err)
os.Exit(1)
}
}
Notable apps built with BubbleTea include Crush (Charm’s TUI-based AI coding agent), Glow, Huh, and many tools from the top trending Go projects ecosystem. For more complex Go apps you might add dependency injection and solid unit tests; the same ideas apply to BubbleTea models and commands.
What is Ratatui?
Ratatui is a Rust library for TUIs that uses immediate-mode rendering: each frame you describe the entire UI (widgets and layout), and Ratatui draws it. What is Ratatui? It’s a lightweight, unopinionated toolkit—it doesn’t impose an Elm-style model or a specific app structure. You keep your own state, run your own event loop (typically with crossterm, termion, or termwiz), and call terminal.draw(|f| { ... }) to render. So the difference between Elm-style and immediate mode is exactly this: in Elm-style the framework owns the loop and you react via Update/View; in immediate mode you own the loop and redraw the whole UI from current state every frame.
Ratatui is used by 2,100+ crates and trusted by companies like Netflix (e.g. bpftop), OpenAI, AWS (e.g. amazon-q-developer-cli), and Vercel. Version 0.30.x is current, with strong docs and optional backends. It’s a good fit when you want full control over input and rendering, or when you’re already in the Rust ecosystem.
A minimal Ratatui app: you init the terminal, run a loop that draws and then reads events, and restore the terminal on exit.
use crossterm::event::{self, Event, KeyCode};
use ratatui::{prelude::*, widgets::Paragraph};
use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut terminal = ratatui::init();
loop {
terminal.draw(|frame| {
let area = frame.area();
frame.render_widget(
Paragraph::new("Hello, Ratatui! Press q to quit.").alignment(Alignment::Center),
area,
);
})?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
break;
}
}
}
}
ratatui::restore();
Ok(())
}
So: when should I choose BubbleTea vs Ratatui? Pick BubbleTea when you want the fastest path to a polished TUI in Go, with a single model and clear Update/View, and when your team or ecosystem is Go (e.g. you’re already using Go ORMs or Ollama in Go). Pick Ratatui when you need maximum control, are in Rust, or are building performance-sensitive or resource-constrained TUIs; its immediate-mode design and optional backends gives that flexibility.
Summary
| Aspect | BubbleTea (Go) | Ratatui (Rust) |
|---|---|---|
| Style | Elm Architecture (model, Init/Update/View) | Immediate mode (you own loop, draw each frame) |
| Ecosystem | Bubbles, Lip Gloss, 10k+ apps, Crush | 2,100+ crates, Netflix/OpenAI/AWS/Vercel |
| Best for | Rapid iteration, Go teams, CLIs | Control, performance, Rust codebases |
Both are excellent choices; preferrend language and how much structure we want from the framework should drive the decision.
Useful links
- Top 19 Trending Go Projects on GitHub - January 2026
- Top 23 Trending Rust Projects on GitHub - January 2026
- Dependency Injection in Go: Patterns & Best Practices
- Go SDKs for Ollama - comparison with examples
- Comparing Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Go Project Structure: Practices & Patterns
- Go Unit Testing: Structure & Best Practices