charm-bubbles

Pre-built TUI components for Bubble Tea apps - spinner, text input, textarea, list, table, viewport, paginator, progress bar. Use when adding Go TUI components, bubbles, or terminal widgets to a Bubble Tea app. NOT for the core TUI framework (use bubbletea).

6 stars

Best use case

charm-bubbles is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Pre-built TUI components for Bubble Tea apps - spinner, text input, textarea, list, table, viewport, paginator, progress bar. Use when adding Go TUI components, bubbles, or terminal widgets to a Bubble Tea app. NOT for the core TUI framework (use bubbletea).

Teams using charm-bubbles should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/charm-bubbles/SKILL.md --create-dirs "https://raw.githubusercontent.com/alxxpersonal/forge/main/skills/charm-bubbles/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/charm-bubbles/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How charm-bubbles Compares

Feature / Agentcharm-bubblesStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Pre-built TUI components for Bubble Tea apps - spinner, text input, textarea, list, table, viewport, paginator, progress bar. Use when adding Go TUI components, bubbles, or terminal widgets to a Bubble Tea app. NOT for the core TUI framework (use bubbletea).

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Bubbles - TUI Components for Bubble Tea

Bubbles (`charm.land/bubbles/v2`) is a component library for [Bubble Tea](https://github.com/charmbracelet/bubbletea) applications. Each bubble is a self-contained Model with `Update` and `View` methods you embed in your own model.

## Quick Start

Embed a bubble (e.g. spinner) inside your Bubble Tea app:

```go
package main

import (
    "fmt"
    tea "charm.land/bubbletea/v2"
    "charm.land/bubbles/v2/spinner"
)

type model struct {
    spinner spinner.Model
}

func initialModel() model {
    return model{spinner: spinner.New(spinner.WithSpinner(spinner.Dot))}
}

func (m model) Init() tea.Cmd {
    return m.spinner.Tick // start the spinner
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        if msg.String() == "q" {
            return m, tea.Quit
        }
    }
    var cmd tea.Cmd
    m.spinner, cmd = m.spinner.Update(msg)
    return m, cmd
}

func (m model) View() string {
    return fmt.Sprintf("\n  %s Loading...\n", m.spinner.View())
}

func main() {
    tea.NewProgram(initialModel()).Run()
}
```

## How Bubbles Work

### The Update/View Contract

Every bubble follows the same pattern:

1. **Model** - a struct holding component state
2. **New()** - constructor returning a configured Model (usually with functional options)
3. **Update(msg tea.Msg) (Model, tea.Cmd)** - processes messages, returns updated model + commands
4. **View() string** - renders current state to a string

Bubbles return their own Model type from Update (not `tea.Model`), so you assign back to the embedded field:

```go
// Correct: assign back to the field
m.textinput, cmd = m.textinput.Update(msg)

// Wrong: this loses the update
m.textinput.Update(msg)
```

### Message Flow

Bubbles communicate via typed messages. When you embed a bubble, you pass all messages through to its Update:

```go
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    var cmds []tea.Cmd

    // Handle your own messages first
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        // your key handling
    }

    // Forward to embedded bubbles
    var cmd tea.Cmd
    m.spinner, cmd = m.spinner.Update(msg)
    cmds = append(cmds, cmd)

    m.textinput, cmd = m.textinput.Update(msg)
    cmds = append(cmds, cmd)

    return m, tea.Batch(cmds...)
}
```

### Commands and Init

Some bubbles require commands to start (spinner needs `Tick`, timer needs `Init`). Return these from your top-level `Init()`:

```go
func (m model) Init() tea.Cmd {
    return tea.Batch(
        m.spinner.Tick,
        m.timer.Init(),
    )
}
```

## Common Patterns

### Focus Management

Interactive bubbles (textinput, textarea, table, filepicker) have Focus/Blur methods. Only focused components process keyboard input.

```go
// Focus returns a tea.Cmd for textinput (starts cursor blinking)
cmd := m.textinput.Focus()

// Table focus is simpler, no cmd needed
m.table.Focus()

// Blur removes focus
m.textinput.Blur()
```

When managing multiple inputs, blur all then focus the active one:

```go
for i := range m.inputs {
    m.inputs[i].Blur()
}
m.inputs[m.focusIndex].Focus()
```

### Functional Options

Most constructors accept variadic options:

```go
s := spinner.New(spinner.WithSpinner(spinner.Dot), spinner.WithStyle(myStyle))
t := table.New(table.WithColumns(cols), table.WithRows(rows), table.WithHeight(10))
v := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
p := progress.New(progress.WithDefaultBlend(), progress.WithoutPercentage())
tmr := timer.New(30*time.Second, timer.WithInterval(100*time.Millisecond))
```

### Styling with Lipgloss

All visual bubbles accept lipgloss styles. Common pattern:

```go
// Spinner: direct Style field
s := spinner.New()
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))

// Table: Styles struct
t := table.New(table.WithStyles(table.Styles{
    Header:   lipgloss.NewStyle().Bold(true).Padding(0, 1),
    Cell:     lipgloss.NewStyle().Padding(0, 1),
    Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")),
}))

// TextInput/TextArea: SetStyles method with Focused/Blurred states
ti := textinput.New()
ti.SetStyles(textinput.DefaultDarkStyles())

// Viewport: Style field for borders/padding
vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
vp.Style = lipgloss.NewStyle().Border(lipgloss.RoundedBorder())
```

### Key Bindings

Use the `key` package for remappable bindings that integrate with the `help` bubble:

```go
type KeyMap struct {
    Quit key.Binding
    Help key.Binding
}

var keys = KeyMap{
    Quit: key.NewBinding(
        key.WithKeys("q", "ctrl+c"),
        key.WithHelp("q", "quit"),
    ),
    Help: key.NewBinding(
        key.WithKeys("?"),
        key.WithHelp("?", "help"),
    ),
}

// In Update:
case tea.KeyPressMsg:
    switch {
    case key.Matches(msg, keys.Quit):
        return m, tea.Quit
    }

// For help integration, implement help.KeyMap interface:
func (k KeyMap) ShortHelp() []key.Binding { return []key.Binding{k.Quit, k.Help} }
func (k KeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{{k.Quit, k.Help}} }
```

### Composing Multiple Bubbles

The list component is a good example of composition - it internally uses spinner, textinput, paginator, and help:

```go
type model struct {
    list     list.Model
    viewport viewport.Model
    help     help.Model
    spinner  spinner.Model
}
```

Combine their views with lipgloss layout:

```go
func (m model) View() string {
    left := m.list.View()
    right := m.viewport.View()
    return lipgloss.JoinHorizontal(lipgloss.Top, left, right)
}
```

### Window Size Handling

Resize bubbles when the terminal size changes:

```go
case tea.WindowSizeMsg:
    m.viewport.SetWidth(msg.Width)
    m.viewport.SetHeight(msg.Height - headerHeight)
    m.list.SetSize(msg.Width, msg.Height)
    m.table.SetWidth(msg.Width)
    m.table.SetHeight(msg.Height)
    m.progress.SetWidth(msg.Width - padding)
    m.help.SetWidth(msg.Width)
```

### ID-Based Message Routing

Animated bubbles (spinner, progress, timer, stopwatch) use internal IDs so multiple instances don't interfere. Each instance only processes messages with its own ID. Just forward all messages to all instances:

```go
m.spinner1, cmd1 = m.spinner1.Update(msg)
m.spinner2, cmd2 = m.spinner2.Update(msg)
```

## Integration with Bubbletea and Lipgloss

### Import Paths (v2)

```go
import (
    tea "charm.land/bubbletea/v2"
    "charm.land/bubbles/v2/spinner"
    "charm.land/bubbles/v2/textinput"
    "charm.land/lipgloss/v2"
)
```

### Key Message Types

Bubbles v2 uses `tea.KeyPressMsg` (not `tea.KeyMsg` from v1). Match with `key.Matches`:

```go
case tea.KeyPressMsg:
    switch {
    case key.Matches(msg, m.KeyMap.Up):
        // handle up
    }
```

### Progress Bar - Static vs Animated

Progress supports two modes:

```go
// Animated: use SetPercent (returns cmd), Update processes FrameMsg
cmd := m.progress.SetPercent(0.75)

// Static: use ViewAs directly, no Update needed
view := m.progress.ViewAs(0.75)
```

### List Item Interface

The list component requires items to implement the `Item` interface:

```go
type Item interface {
    FilterValue() string
}
```

And a delegate implementing `ItemDelegate`:

```go
type ItemDelegate interface {
    Render(w io.Writer, m Model, index int, item Item)
    Height() int
    Spacing() int
    Update(msg tea.Msg, m *Model) tea.Cmd
}
```

### Filepicker Selection

Check for file selection in your Update:

```go
case tea.KeyPressMsg:
    m.filepicker, cmd = m.filepicker.Update(msg)
    if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
        m.selectedFile = path
    }
```

## Common Mistakes

1. **Not returning commands from Update.** Bubbles like spinner and timer stop working if you drop their commands. Always capture and return: `m.spinner, cmd = m.spinner.Update(msg)`

2. **Forgetting to call Init/Tick.** Spinner needs `m.spinner.Tick` returned from Init. Timer needs `m.timer.Init()`. Without these, the component never starts animating.

3. **Not assigning Update result back.** Bubbles return value types (not pointers). `m.spinner.Update(msg)` without assignment discards the update.

4. **Forwarding messages only to focused bubble.** Most bubbles self-filter (spinners ignore wrong IDs, unfocused inputs ignore keys). Forward all messages to all bubbles and let them decide.

5. **Using v1 message types.** In v2, it's `tea.KeyPressMsg` not `tea.KeyMsg`. Check the upgrade guide if migrating.

6. **Not handling WindowSizeMsg.** Components with fixed dimensions (viewport, list, table, progress) need resizing or they clip/overflow.

7. **Setting width/height to 0.** Viewport and table render empty strings when dimensions are 0. Always set dimensions before first render.

8. **Calling Focus() without using the cmd.** `textinput.Focus()` returns a `tea.Cmd` for cursor blinking. If you drop it, the cursor won't blink.

## Checklist

- [ ] Embed bubble Model as a field in your model (not a pointer)
- [ ] Call constructor with `New()` or `New(opts...)`
- [ ] Return Init commands from your `Init()` (Tick for spinner, Init for timer/stopwatch)
- [ ] Forward messages to bubble's `Update` and capture both return values
- [ ] Collect commands with `tea.Batch` when using multiple bubbles
- [ ] Call `Focus()` on interactive components and use returned cmd
- [ ] Handle `tea.WindowSizeMsg` to resize dimension-aware components
- [ ] Implement `Item` and `ItemDelegate` interfaces when using list
- [ ] Use `key.Matches(msg, binding)` for key matching in v2
- [ ] Style with lipgloss via Style fields or SetStyles methods

Related Skills

charm-vhs

6
from alxxpersonal/forge

Record terminal sessions as GIF/MP4/WebM from declarative .tape scripts with VHS. Use when creating terminal demos, recording CLI sessions, VHS tape files, or generating terminal GIFs.

charm-ultraviolet

6
from alxxpersonal/forge

Low-level Go terminal primitives - cell-based rendering, input handling, screen management. Use when building custom Go terminal renderers, ultraviolet, cell buffers, or performance-critical TUI work below Bubble Tea's abstraction level.

charm-pop

6
from alxxpersonal/forge

Send emails from the terminal with pop - TUI and CLI modes, SMTP and Resend support, attachments. Use when sending email from terminal, pop, CLI email, or piping email content from shell scripts.

charm-lipgloss

6
from alxxpersonal/forge

CSS-like terminal styling for Go with lipgloss v2 - styles, colors, borders, layout, tables, lists, and trees. Use when styling Go terminal output, lipgloss, terminal layout composition, or building styled tables/lists/trees in Go.

charm-huh

6
from alxxpersonal/forge

Build interactive terminal forms and prompts in Go with huh - input, select, confirm, multiselect, validation, theming. Use when building Go terminal forms, huh, interactive Go prompts, or form fields with validation. NOT for shell script prompts (use gum).

charm-harmonica

6
from alxxpersonal/forge

Physics-based animation for Go TUIs - damped spring oscillator and projectile motion. Use when adding spring animations, physics-based motion, or smooth transitions to Go terminal apps. No Ease function exists in this library.

charm-gum

6
from alxxpersonal/forge

Interactive shell script prompts, fuzzy filters, spinners, and styled output with gum. Use when building bash/shell script UIs, gum commands, interactive shell prompts, or CLI script workflows. NOT for Go terminal forms (use huh).

charm-freeze

6
from alxxpersonal/forge

Generate PNG, SVG, or WebP screenshots of code and terminal output with freeze. Use when screenshotting code, freeze, terminal-to-image, or capturing styled code snippets as images.

charm-fang

6
from alxxpersonal/forge

Wrap Cobra with styled help, error output, auto versioning, and manpage generation via fang. Use when building Go CLIs with fang, styled Cobra help, or adding lipgloss-rendered help pages to a Go CLI.

charm-ecosystem

6
from alxxpersonal/forge

Architect's guide to the charmbracelet Go TUI ecosystem - which libraries to combine, dependency hierarchy, integration patterns. Use when choosing charm libraries, planning TUI architecture, combining bubbletea/lipgloss/bubbles/huh, or asking 'which charm library for X'. NOT for specific library API details (use individual charm-* skills).

charm-crush

6
from alxxpersonal/forge

Architecture patterns from Crush, charmbracelet's production agentic coding CLI built on bubbletea v2, lipgloss v2, bubbles v2, glamour v2, and ultraviolet. Use when building production bubbletea apps, composing charm TUI components at scale, designing agentic CLI tools, implementing streaming LLM UIs, or asking about crush internals. NOT for individual charm library basics.

charm-bubbletea

6
from alxxpersonal/forge

Build terminal UIs in Go with Bubble Tea v2's Elm Architecture (Model/Update/View). Use when building Go TUI apps, tea.Model, tea.Cmd, Elm architecture, or terminal applications. NOT for pre-built TUI components (use bubbles).