Bubbles
GitHub Overview
charmbracelet/bubbles
TUI components for Bubble Tea 🫧
Repository:https://github.com/charmbracelet/bubbles
Stars6,625
Watchers23
Forks319
Created:January 18, 2020
Language:Go
License:MIT License
Topics
clielm-architecturehacktoberfestterminaltui
Star History
Data as of: 7/25/2025, 11:09 AM
Bubbles
Bubbles is the official component library for the Bubble Tea framework. It provides common TUI components such as text input, lists, spinners, and progress bars, significantly accelerating development. As part of the Charmbracelet ecosystem, high-quality components are continuously added.
Features
Rich Components
- Input: textinput, textarea, textinput
- Selection: list, filepicker, table
- Display: viewport, spinner, progress
- Navigation: paginator, help
- Special: timer, stopwatch, cursor
Bubble Tea Integration
- Model-based: Follows Elm Architecture
- Message Passing: Communication between components
- Customizable: Adjust appearance and behavior
- Composable: Integrate multiple components
Styling
- Lip Gloss Integration: Beautiful default styles
- Theme Support: Apply custom themes
- Adaptive: Adjusts based on terminal capabilities
- Animations: Smooth operations
Productivity
- Ready to Use: Works with minimal configuration
- Well Documented: Detailed usage examples
- Tested: High-quality code
- Maintained: Continuous updates by Charmbracelet
Basic Usage
Installation
go get github.com/charmbracelet/bubbles
Text Input
package main
import (
"fmt"
"os"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
)
type model struct {
textInput textinput.Model
err error
}
func initialModel() model {
ti := textinput.New()
ti.Placeholder = "Enter your name"
ti.Focus()
ti.CharLimit = 156
ti.Width = 20
return model{
textInput: ti,
err: nil,
}
}
func (m model) Init() tea.Cmd {
return textinput.Blink
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyEnter, tea.KeyCtrlC, tea.KeyEsc:
return m, tea.Quit
}
case error:
m.err = msg
return m, nil
}
m.textInput, cmd = m.textInput.Update(msg)
return m, cmd
}
func (m model) View() string {
return fmt.Sprintf(
"What's your name?\n\n%s\n\n%s",
m.textInput.View(),
"(esc to quit)",
) + "\n"
}
func main() {
if _, err := tea.NewProgram(initialModel()).Run(); err != nil {
fmt.Printf("could not start program: %s\n", err)
os.Exit(1)
}
}
List Component
package main
import (
"fmt"
"os"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var docStyle = lipgloss.NewStyle().Margin(1, 2)
type item string
func (i item) FilterValue() string { return string(i) }
type model struct {
list list.Model
}
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:
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
case tea.WindowSizeMsg:
h, v := docStyle.GetFrameSize()
m.list.SetSize(msg.Width-h, msg.Height-v)
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
func (m model) View() string {
return docStyle.Render(m.list.View())
}
func main() {
items := []list.Item{
item("🍃 Bubble Tea"),
item("🌈 Lip Gloss"),
item("🍕 Bubbles"),
item("🎆 Charm"),
item("🍓 Glow"),
item("🍰 Soft Serve"),
}
m := model{list: list.New(items, list.NewDefaultDelegate(), 0, 0)}
m.list.Title = "Charmbracelet Tools"
p := tea.NewProgram(m, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
Progress Bar and Spinner
package main
import (
"fmt"
"os"
"strings"
"time"
"github.com/charmbracelet/bubbles/progress"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type model struct {
spinner spinner.Model
progress progress.Model
done bool
}
var (
currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211"))
doneStyle = lipgloss.NewStyle().Margin(1, 2)
checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓")
)
func newModel() model {
p := progress.New(
progress.WithDefaultGradient(),
progress.WithWidth(40),
progress.WithoutPercentage(),
)
s := spinner.New()
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
return model{
spinner: s,
progress: p,
}
}
func (m model) Init() tea.Cmd {
return tea.Batch(
spinner.Tick,
tickProgress(),
)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
return m, tea.Quit
case tea.WindowSizeMsg:
m.progress.Width = msg.Width - 4
if m.progress.Width > 40 {
m.progress.Width = 40
}
return m, nil
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
case progressMsg:
var cmds []tea.Cmd
if msg >= 1.0 {
m.done = true
return m, tea.Quit
}
var cmd tea.Cmd
m.progress, cmd = m.progress.Update(msg)
cmds = append(cmds, cmd, tickProgress())
return m, tea.Batch(cmds...)
}
return m, nil
}
func (m model) View() string {
if m.done {
return doneStyle.Render(fmt.Sprintf("%s Done!\n", checkMark))
}
pkgName := currentPkgNameStyle.Render("bubbles")
prog := m.progress.View()
cellsRemaining := m.progress.Width - lipgloss.Width(prog)
gap := strings.Repeat(" ", cellsRemaining)
return "\n" +
m.spinner.View() + " Installing " + pkgName + gap + prog + "\n\n" +
"Press any key to quit\n"
}
type progressMsg float64
func tickProgress() tea.Cmd {
return tea.Tick(time.Millisecond*100, func(t time.Time) tea.Msg {
return progressMsg(time.Since(time.Now()).Seconds() / 3)
})
}
func main() {
if _, err := tea.NewProgram(newModel()).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
Table Component
package main
import (
"fmt"
"os"
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var baseStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240"))
type model struct {
table table.Model
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
if m.table.Focused() {
m.table.Blur()
} else {
m.table.Focus()
}
case "q", "ctrl+c":
return m, tea.Quit
}
}
m.table, cmd = m.table.Update(msg)
return m, cmd
}
func (m model) View() string {
return baseStyle.Render(m.table.View()) + "\n"
}
func main() {
columns := []table.Column{
{Title: "Rank", Width: 4},
{Title: "City", Width: 10},
{Title: "Country", Width: 10},
{Title: "Population", Width: 10},
}
rows := []table.Row{
{"1", "Tokyo", "Japan", "37,274,000"},
{"2", "Delhi", "India", "32,065,760"},
{"3", "Shanghai", "China", "28,516,904"},
{"4", "Dhaka", "Bangladesh", "22,478,116"},
{"5", "São Paulo", "Brazil", "22,429,800"},
}
t := table.New(
table.WithColumns(columns),
table.WithRows(rows),
table.WithFocused(true),
table.WithHeight(7),
)
s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderBottom(true).
Bold(false)
s.Selected = s.Selected.
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("57")).
Bold(false)
t.SetStyles(s)
m := model{t}
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
Advanced Features
File Picker
import "github.com/charmbracelet/bubbles/filepicker"
fp := filepicker.New()
fp.AllowedTypes = []string{".txt", ".md", ".go"}
fp.CurrentDirectory, _ = os.Getwd()
Viewport
import "github.com/charmbracelet/bubbles/viewport"
vp := viewport.New(80, 20)
vp.SetContent("Long text content...")
vp.KeyMap = viewport.DefaultKeyMap()
Timer and Stopwatch
import (
"github.com/charmbracelet/bubbles/timer"
"github.com/charmbracelet/bubbles/stopwatch"
)
// Timer
t := timer.NewWithInterval(5*time.Minute, time.Second)
// Stopwatch
sw := stopwatch.NewWithInterval(time.Millisecond * 100)
Creating Custom Components
type customInput struct {
textinput.Model
label string
err error
}
func NewCustomInput(label string) customInput {
ti := textinput.New()
ti.Placeholder = "Enter " + label
ti.CharLimit = 50
ti.Width = 30
return customInput{
Model: ti,
label: label,
}
}
func (c customInput) View() string {
return fmt.Sprintf("%s: %s", c.label, c.Model.View())
}
Ecosystem
Related Projects
- Bubble Tea: Base framework
- Lip Gloss: Styling library
- Harmonica: Animation library
- Log: Logging library
Adoption Examples
- GitHub CLI: Used in official tools
- Charm Tools: Glow, Soft Serve, etc.
- Community Projects: Many TUI apps
Advantages
- Productivity: Ready-to-use components
- Quality: Tested and stable
- Consistency: Unified API design
- Customizable: Flexible extensibility
- Maintenance: Active development
Limitations
- Bubble Tea Dependent: Doesn't work standalone
- Learning Curve: Requires understanding of Elm Architecture
- Component Limited: Need to build missing features
Comparison with Other Libraries
Feature | Bubbles | tview | termui |
---|---|---|---|
Architecture | Elm | Widget | Widget |
Component Count | Medium | Many | Medium |
Customizability | Very High | Medium | Low |
Learning Cost | Medium | Low | Low-Medium |
Ecosystem | Charmbracelet | Independent | Independent |
Summary
Bubbles is an essential component library when building TUI applications with the Bubble Tea framework. With high-quality components provided, developers can focus on application logic, significantly improving productivity. As part of the Charmbracelet ecosystem, continuous improvements and new component additions can be expected.