Essay
Building Beautiful Terminal UIs with Bubble Tea
A practical guide to building interactive terminal applications in Go using the Bubble Tea framework and the Charm ecosystem.
Ported from the gZen learning journal into DivineForge essays — engineering writing lives here now.
The terminal doesn't have to be boring. With Bubble Tea, you can build rich, interactive terminal user interfaces (TUIs) in Go using an elegant Elm-inspired architecture.
Why Bubble Tea?
Before Bubble Tea, building TUIs in Go meant reaching for tview, termui, or raw ncurses bindings. These work, but they often feel imperative and stateful. Bubble Tea brings a functional, declarative model to terminal UI:
- Model — your application state
- Update — a function that returns a new model in response to messages
- View — a function that renders your model as a string
This unidirectional data flow makes reasoning about complex terminal apps much easier.
Your First Bubble Tea App
Let's build a minimal counter app:
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
type model int
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":
return m + 1, nil
case "down":
return m - 1, nil
}
}
return m, nil
}
func (m model) View() string {
return fmt.Sprintf("Count: %d\n\n↑ increment · ↓ decrement · q quit", int(m))
}
func main() {
p := tea.NewProgram(model(0))
if _, err := p.Run(); err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
}
Run with go run main.go and you have a working interactive TUI in under 50 lines.
The Charm Ecosystem
Bubble Tea is part of the larger Charm ecosystem:
| Library | Purpose |
|---|---|
| Bubble Tea | TUI framework (Model-View-Update) |
| Lip Gloss | Styling — colors, alignment, margins |
| Bubbles | Pre-built components (text input, spinner, progress bar) |
| Glamour | Render Markdown in the terminal |
| Wish | SSH-based TUIs (remote apps over SSH) |
Adding Lip Gloss Styling
import "github.com/charmbracelet/lipgloss"
var style = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#FAFAFA")).
Background(lipgloss.Color("#7C3AED")).
Padding(1, 2).
Margin(1, 0)
func (m model) View() string {
return style.Render(fmt.Sprintf("Count: %d", int(m)))
}
Practical Patterns
Timers and Ticks
Bubble Tea uses commands (tea.Cmd) for side effects. A common pattern is the tick command for periodic updates:
type tickMsg time.Time
func tickEvery() tea.Cmd {
return tea.Every(time.Second, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tickMsg:
// Update model every second
return m, tickEvery() // Re-schedule
}
return m, nil
}
Working with Windows
Bubble Tea handles terminal resize events automatically:
type model struct {
width int
height int
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
}
return m, nil
}
When to Use Bubble Tea
| Use Case | Recommendation |
|---|---|
| Interactive CLI (forms, wizards) | Bubble Tea |
| Simple command-line tool | Cobra + standard flags |
| Dashboard / monitoring | Bubble Tea + Lip Gloss |
| File manager / editor | Bubble Tea + Bubbles |
| SSH-based app | Bubble Tea + Wish |
Resources
The terminal is a canvas — Bubble Tea gives you the brushes.