Bubbles

TUIComponentsBubble-TeaTerminalGo

GitHub Overview

charmbracelet/bubbles

TUI components for Bubble Tea 🫧

Stars6,625
Watchers23
Forks319
Created:January 18, 2020
Language:Go
License:MIT License

Topics

clielm-architecturehacktoberfestterminaltui

Star History

charmbracelet/bubbles 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

FeatureBubblestviewtermui
ArchitectureElmWidgetWidget
Component CountMediumManyMedium
CustomizabilityVery HighMediumLow
Learning CostMediumLowLow-Medium
EcosystemCharmbraceletIndependentIndependent

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.