Bubble Tea
GitHub Overview
charmbracelet/bubbletea
A powerful little TUI framework 🏗
Repository:https://github.com/charmbracelet/bubbletea
Stars33,527
Watchers123
Forks945
Created:January 10, 2020
Language:Go
License:MIT License
Topics
clielm-architectureframeworkfunctionalgogolanghacktoberfesttui
Star History
Data as of: 7/25/2025, 11:09 AM
Bubble Tea
Bubble Tea is a modern TUI framework based on The Elm Architecture. It leverages functional programming concepts to express state management and UI updates declaratively. As the core of the Charmbracelet ecosystem, it has become the de facto standard for TUI development in the Go language.
Features
The Elm Architecture
- Model: Represents the application state
- View: Renders UI based on the Model
- Update: Updates Model in response to messages
- Message-driven: Clear separation of events and state changes
Performance and Features
- Frame-rate based: Smooth animations and rendering
- Mouse support: Click, wheel, and motion events
- Focus management: Focus event tracking
- Async I/O: Leverages Go's concurrency
- Production-ready: Adopted by numerous production applications
Developer Experience
- Simple API: Intuitive and easy to learn
- Testability: Easy testing with pure functions and immutability
- Debug features: Built-in debug mode
- Extensibility: Custom renderers and middleware
Charmbracelet Ecosystem
- Lip Gloss: Styling library
- Bubbles: Reusable component collection
- Glamour: Markdown rendering
- Charm: Backend services
Basic Usage
Installation
go get github.com/charmbracelet/bubbletea
Hello World
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
// Define Model
type model struct {
cursor int
choices []string
selected map[int]struct{}
}
// Initialize function
func initialModel() model {
return model{
choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
selected: make(map[int]struct{}),
}
}
// Init command
func (m model) Init() tea.Cmd {
return nil
}
// Update function
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", " ":
_, ok := m.selected[m.cursor]
if ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
return m, nil
}
// View function
func (m model) View() string {
s := "What should we buy at the market?\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)
}
s += "\nPress q to quit.\n"
return s
}
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Printf("Alas, there's been an error: %v\n", err)
os.Exit(1)
}
}
Commands and Messages
package main
import (
"time"
tea "github.com/charmbracelet/bubbletea"
)
// Custom message
type tickMsg time.Time
// Command definition
func tickCmd() tea.Cmd {
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
type model struct {
lastTick time.Time
}
func (m model) Init() tea.Cmd {
return tickCmd()
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "q" || msg.String() == "ctrl+c" {
return m, tea.Quit
}
case tickMsg:
m.lastTick = time.Time(msg)
return m, tickCmd()
}
return m, nil
}
func (m model) View() string {
return "The time is " + m.lastTick.Format("15:04:05")
}
Styling with Lip Gloss
package main
import (
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
// Style definitions
titleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#7D56F4")).
PaddingTop(1).
PaddingBottom(1)
itemStyle = lipgloss.NewStyle().
PaddingLeft(4)
selectedItemStyle = lipgloss.NewStyle().
PaddingLeft(2).
Foreground(lipgloss.Color("170"))
helpStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("241")).
PaddingTop(1)
)
type model struct {
spinner spinner.Model
loading bool
items []string
cursor int
}
func initialModel() model {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
return model{
spinner: s,
loading: true,
items: []string{},
}
}
func (m model) View() string {
if m.loading {
return "\n" + m.spinner.View() + " Loading..."
}
s := titleStyle.Render("🧋 Bubble Tea Example")
s += "\n\n"
for i, item := range m.items {
if m.cursor == i {
s += selectedItemStyle.Render("▶ " + item) + "\n"
} else {
s += itemStyle.Render(item) + "\n"
}
}
s += helpStyle.Render("\n↑/↓: navigate • q: quit")
return s
}
Advanced Features
Async I/O
package main
import (
"fmt"
"net/http"
"time"
tea "github.com/charmbracelet/bubbletea"
)
type responseMsg struct {
status int
err error
}
func checkServer(url string) tea.Cmd {
return func() tea.Msg {
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(url)
if err != nil {
return responseMsg{err: err}
}
defer resp.Body.Close()
return responseMsg{status: resp.StatusCode}
}
}
type model struct {
url string
checking bool
status int
err error
}
func (m model) Init() tea.Cmd {
return checkServer(m.url)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case responseMsg:
m.checking = false
m.status = msg.status
m.err = msg.err
return m, nil
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "r":
m.checking = true
return m, checkServer(m.url)
}
}
return m, nil
}
Custom Renderer
package main
import (
tea "github.com/charmbracelet/bubbletea"
)
// Custom renderer
type customRenderer struct {
tea.StandardRenderer
frames int
}
func (r *customRenderer) Render(v string) {
r.frames++
// Add FPS and performance info
v += fmt.Sprintf("\n\nFrames: %d", r.frames)
r.StandardRenderer.Render(v)
}
func main() {
renderer := &customRenderer{
StandardRenderer: tea.StandardRenderer{},
}
p := tea.NewProgram(
initialModel(),
tea.WithRenderer(renderer),
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
)
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
Using Bubbles Components
package main
import (
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
)
type model struct {
textInput textinput.Model
viewport viewport.Model
list list.Model
ready bool
}
func initialModel() model {
// Text input
ti := textinput.New()
ti.Placeholder = "Type something..."
ti.Focus()
ti.CharLimit = 156
ti.Width = 20
// List
items := []list.Item{
item{"Item 1", "Description 1"},
item{"Item 2", "Description 2"},
item{"Item 3", "Description 3"},
}
l := list.New(items, itemDelegate{}, 20, 14)
l.Title = "My List"
l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
return model{
textInput: ti,
list: l,
}
}
Ecosystem
Official Tools
- Glow: Markdown viewer
- Soft Serve: Git server
- VHS: Terminal recorder
- Charm: Cloud services
Community Projects
- lazygit: Git TUI
- duf: Disk usage tool
- gdu: Disk usage analyzer
- ticker: Stock tracker
Advantages
- Modern Design: Excellent design based on The Elm Architecture
- Active Development: Continuous improvements by Charmbracelet
- Rich Ecosystem: Comprehensive components and tools
- Testability: Pure functions and immutability
- Performance: Efficient rendering and updates
Limitations
- Learning Curve: Requires understanding of The Elm Architecture
- Framework-specific: Can be difficult to integrate with other approaches
- Community Dependent: Relies on Bubbles components
Comparison with Other Libraries
Feature | Bubble Tea | tview | termui |
---|---|---|---|
Architecture | Elm | Widget-based | Dashboard |
Learning Cost | Medium | Low | Low-Medium |
Ecosystem | Very Large | Large | Medium |
Flexibility | Very High | High | Medium |
Performance | Very High | High | High |
Summary
Bubble Tea is the de facto standard for modern TUI development in Go. With its excellent design based on The Elm Architecture, active development community, and rich ecosystem, it's suitable for a wide range of applications from small tools to large-scale applications. It's particularly optimal for projects that prioritize maintainability and testability.