Skip to content

Bubble Tea

Developer UtilitiesCLI/UtilsGo

What it is

Bubble Tea is a framework for building terminal user interfaces in Go, based on the Elm architecture of model, update and view.

Implement a Model with Init, Update and View. Update receives messages — key presses, window resizes, custom events — and returns a new model plus optional commands.

Installation

go get github.com/charmbracelet/bubbletea

Getting started

The smallest useful thing you can do with it, and what each part means.

The Elm architecture in Go
type model struct {
    choices  []string
    cursor   int
    selected map[int]bool
}

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", " ":
            m.selected[m.cursor] = !m.selected[m.cursor]
        }
    }
    return m, nil
}

func (m model) View() string {
    s := "Select books:\n\n"
    for i, choice := range m.choices {
        cursor := " "
        if m.cursor == i { cursor = ">" }
        checked := " "
        if m.selected[i] { checked = "x" }
        s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
    }
    return s + "\nq to quit\n"
}
The model is immutable by convention: Update returns a modified copy rather than mutating in place. View is a pure function of state, so rendering is always consistent.

Advanced usage

Where the library earns its place over a simpler alternative.

Async work via commands
type booksMsg []Book

func fetchBooks() tea.Cmd {
    return func() tea.Msg {
        books, err := api.List()   // runs off the UI loop
        if err != nil {
            return errMsg{err}
        }
        return booksMsg(books)
    }
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case booksMsg:
        m.books = msg
        m.loading = false
    }
    return m, nil
}
Never block inside Update — the UI freezes. Return a tea.Cmd instead; Bubble Tea runs it in a goroutine and delivers the result back as a message.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

The interface freezes
Update is doing blocking work. Move it into a tea.Cmd.
Output is garbled after exit
The program exited without restoring the terminal. Return the error from p.Run() and avoid os.Exit inside the model.

Best practices

  • Never perform I/O directly in Update; return a tea.Cmd so the UI stays responsive.
  • Always handle ctrl+c so the program can be interrupted.
  • Handle tea.WindowSizeMsg to lay out correctly when the terminal is resized.
  • Pair with Lip Gloss for styling rather than writing ANSI escape codes by hand.

Background

Why it exists, and what it was reacting to.

From Charm, Bubble Tea brought a principled state-management model to terminal UIs. Combined with Lip Gloss for styling, it is behind a wave of unusually polished Go CLI tools.