Building Terminal UIs with Charm
If you’ve ever used lazygit, k9s, or htop, you’ve experienced what a well-built terminal UI can feel like — responsive, structured, and surprisingly elegant. Building something like that used to mean wrestling with ncurses or writing raw escape codes. The Charm ecosystem changes that entirely.
This article walks through how to build terminal UIs with Charm using Go, focusing on Bubble Tea and its companion libraries. The current major versions of the Charm libraries use updated Go module paths, which you’ll see reflected in the examples below. If you think in components and state, the mental model will feel immediately familiar.
Key Takeaways
- Bubble Tea follows the Elm architecture (Model, Update, View), making state management predictable and composable.
- Lip Gloss provides declarative styling for terminal output, while Bubbles offers ready-made UI components like text inputs, spinners, and viewports.
- The Charm ecosystem’s libraries layer cleanly on top of each other, letting you build polished TUIs in Go without raw escape codes or ncurses.
- Huh and Wish extend the stack for forms and SSH-hosted TUIs, respectively.
What Is a TUI and Why Build One?
A Terminal User Interface (TUI) is an interactive, visually structured application that runs inside a terminal. Unlike a plain CLI that accepts a command and exits, a TUI maintains state, responds to keyboard input in real time, and renders dynamic layouts.
TUIs are worth building when you want:
- A developer tool that works over SSH without a browser
- Lower resource overhead than an Electron app
- Something scriptable that still feels polished
The Charm Ecosystem at a Glance
Charm maintains several libraries that work well together:
| Library | Role |
|---|---|
| Bubble Tea | Application framework (event loop, state) |
| Lip Gloss | Styling and layout |
| Bubbles | Prebuilt UI components |
| Huh | Form and input primitives |
| Wish | SSH server for hosting TUIs remotely |
Bubble Tea is the core. Everything else layers on top of it.
How Bubble Tea’s Architecture Works (Model, Update, View)
Bubble Tea follows the Elm architecture, a pattern frontend developers will recognize from Redux or React’s useReducer.
Every Bubble Tea app defines three things:
- Model — your application state
- Update — a function that handles messages and returns a new model
- View — a function that renders the model as a terminal view
package main
import (
"fmt"
"log"
tea "charm.land/bubbletea/v2"
)
type model struct {
count 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.KeyPressMsg:
switch msg.String() {
case "up":
m.count++
case "q", "ctrl+c":
return m, tea.Quit
}
}
return m, nil
}
func (m model) View() tea.View {
return tea.NewView(fmt.Sprintf("Count: %d\n(press up to increment, q to quit)", m.count))
}
func main() {
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
Notice the charm.land/bubbletea/v2 import path. Current releases of the Charm libraries use charm.land module paths rather than the older github.com/charmbracelet/* imports that appear in many older tutorials.
Adding Layouts and Styling with Lip Gloss
Lip Gloss handles styling. You define styles as values and apply them to strings before rendering.
import "charm.land/lipgloss/v2"
var titleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#FF79C6")).
Padding(0, 1)
func (m model) View() tea.View {
return tea.NewView(titleStyle.Render("My TUI App"))
}
Note: Recent versions of Lip Gloss changed how adaptive color behavior works. Background detection and style adaptation are now handled more explicitly in application code rather than automatically by the library.
Discover how at OpenReplay.com.
Using Prebuilt Components from Bubbles
Bubbles provides ready-made components — text inputs, spinners, progress bars, viewports, and more. Each component follows the same Model/Update/View contract, so you can embed them directly into your own model.
import "charm.land/bubbles/v2/textinput"
type model struct {
input textinput.Model
}
This composability is what makes Bubble Tea scale cleanly. You build small, focused models and compose them into larger ones, exactly like composing components in a frontend framework.
When to Reach for Huh or Wish
If your TUI needs forms — multi-field inputs, confirmations, select menus — Huh handles that without you building it from scratch. For hosting a TUI over SSH so users can access it without installing anything locally, Wish wraps your Bubble Tea program in an SSH server.
Getting Started
go mod init mytui
go get charm.land/bubbletea/v2
go get charm.land/lipgloss/v2
go get charm.land/bubbles/v2
Run your program with:
func main() {
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
Conclusion
The Charm Bubble Tea stack gives you a structured, composable way to build terminal applications in Go. The Model/Update/View pattern keeps state predictable, Lip Gloss handles styling declaratively, and Bubbles gives you components you’d otherwise spend days writing. If you’re comfortable with component-based frontend thinking, building terminal UIs with Charm will feel less like learning a new paradigm and more like applying one you already know — just in a terminal.
FAQs
Bubble Tea is a Go library, so you need at least a basic understanding of Go to use it. That said, the Elm-inspired architecture is straightforward. If you are familiar with concepts like state, messages, and rendering functions from any language, you can pick up the Go-specific syntax relatively quickly.
Current releases use charm.land module paths such as charm.land/bubbletea/v2 instead of the older github.com/charmbracelet imports found in many older tutorials. The Init function returns only a command, and the View function returns a terminal view rather than a raw string.
Yes. Because Update is a pure function that takes a message and returns a new model and command, you can unit test your application logic by calling Update directly with specific messages and asserting on the returned model state. The View function can also be tested by checking its rendered output.
Yes. The Wish library lets you wrap any Bubble Tea program in an SSH server. Users connect via SSH and interact with your TUI directly in their terminal without installing anything locally. This is useful for shared developer tools, dashboards, or interactive demos.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.