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:

LibraryPurpose
Bubble TeaTUI framework (Model-View-Update)
Lip GlossStyling — colors, alignment, margins
BubblesPre-built components (text input, spinner, progress bar)
GlamourRender Markdown in the terminal
WishSSH-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 CaseRecommendation
Interactive CLI (forms, wizards)Bubble Tea
Simple command-line toolCobra + standard flags
Dashboard / monitoringBubble Tea + Lip Gloss
File manager / editorBubble Tea + Bubbles
SSH-based appBubble Tea + Wish

Resources

The terminal is a canvas — Bubble Tea gives you the brushes.