1---
2name: charm-bubbles
3description: "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)."
4---
5
6# Bubbles - TUI Components for Bubble Tea
7
8Bubbles (`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.
9
10## Quick Start
11
12Embed a bubble (e.g. spinner) inside your Bubble Tea app:
13
14```go
15package main
16
17import (
18 "fmt"
19 tea "charm.land/bubbletea/v2"
20 "charm.land/bubbles/v2/spinner"
21)
22
23type model struct {
24 spinner spinner.Model
25}
26
27func initialModel() model {
28 return model{spinner: spinner.New(spinner.WithSpinner(spinner.Dot))}
29}
30
31func (m model) Init() tea.Cmd {
32 return m.spinner.Tick // start the spinner
33}
34
35func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
36 switch msg := msg.(type) {
37 case tea.KeyPressMsg:
38 if msg.String() == "q" {
39 return m, tea.Quit
40 }
41 }
42 var cmd tea.Cmd
43 m.spinner, cmd = m.spinner.Update(msg)
44 return m, cmd
45}
46
47func (m model) View() string {
48 return fmt.Sprintf("\n %s Loading...\n", m.spinner.View())
49}
50
51func main() {
52 tea.NewProgram(initialModel()).Run()
53}
54```
55
56## How Bubbles Work
57
58### The Update/View Contract
59
60Every bubble follows the same pattern:
61
621. **Model** - a struct holding component state
632. **New()** - constructor returning a configured Model (usually with functional options)
643. **Update(msg tea.Msg) (Model, tea.Cmd)** - processes messages, returns updated model + commands
654. **View() string** - renders current state to a string
66
67Bubbles return their own Model type from Update (not `tea.Model`), so you assign back to the embedded field:
68
69```go
70// Correct: assign back to the field
71m.textinput, cmd = m.textinput.Update(msg)
72
73// Wrong: this loses the update
74m.textinput.Update(msg)
75```
76
77### Message Flow
78
79Bubbles communicate via typed messages. When you embed a bubble, you pass all messages through to its Update:
80
81```go
82func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
83 var cmds []tea.Cmd
84
85 // Handle your own messages first
86 switch msg := msg.(type) {
87 case tea.KeyPressMsg:
88 // your key handling
89 }
90
91 // Forward to embedded bubbles
92 var cmd tea.Cmd
93 m.spinner, cmd = m.spinner.Update(msg)
94 cmds = append(cmds, cmd)
95
96 m.textinput, cmd = m.textinput.Update(msg)
97 cmds = append(cmds, cmd)
98
99 return m, tea.Batch(cmds...)
100}
101```
102
103### Commands and Init
104
105Some bubbles require commands to start (spinner needs `Tick`, timer needs `Init`). Return these from your top-level `Init()`:
106
107```go
108func (m model) Init() tea.Cmd {
109 return tea.Batch(
110 m.spinner.Tick,
111 m.timer.Init(),
112 )
113}
114```
115
116## Common Patterns
117
118### Focus Management
119
120Interactive bubbles (textinput, textarea, table, filepicker) have Focus/Blur methods. Only focused components process keyboard input.
121
122```go
123// Focus returns a tea.Cmd for textinput (starts cursor blinking)
124cmd := m.textinput.Focus()
125
126// Table focus is simpler, no cmd needed
127m.table.Focus()
128
129// Blur removes focus
130m.textinput.Blur()
131```
132
133When managing multiple inputs, blur all then focus the active one:
134
135```go
136for i := range m.inputs {
137 m.inputs[i].Blur()
138}
139m.inputs[m.focusIndex].Focus()
140```
141
142### Functional Options
143
144Most constructors accept variadic options:
145
146```go
147s := spinner.New(spinner.WithSpinner(spinner.Dot), spinner.WithStyle(myStyle))
148t := table.New(table.WithColumns(cols), table.WithRows(rows), table.WithHeight(10))
149v := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
150p := progress.New(progress.WithDefaultBlend(), progress.WithoutPercentage())
151tmr := timer.New(30*time.Second, timer.WithInterval(100*time.Millisecond))
152```
153
154### Styling with Lipgloss
155
156All visual bubbles accept lipgloss styles. Common pattern:
157
158```go
159// Spinner: direct Style field
160s := spinner.New()
161s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
162
163// Table: Styles struct
164t := table.New(table.WithStyles(table.Styles{
165 Header: lipgloss.NewStyle().Bold(true).Padding(0, 1),
166 Cell: lipgloss.NewStyle().Padding(0, 1),
167 Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")),
168}))
169
170// TextInput/TextArea: SetStyles method with Focused/Blurred states
171ti := textinput.New()
172ti.SetStyles(textinput.DefaultDarkStyles())
173
174// Viewport: Style field for borders/padding
175vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
176vp.Style = lipgloss.NewStyle().Border(lipgloss.RoundedBorder())
177```
178
179### Key Bindings
180
181Use the `key` package for remappable bindings that integrate with the `help` bubble:
182
183```go
184type KeyMap struct {
185 Quit key.Binding
186 Help key.Binding
187}
188
189var keys = KeyMap{
190 Quit: key.NewBinding(
191 key.WithKeys("q", "ctrl+c"),
192 key.WithHelp("q", "quit"),
193 ),
194 Help: key.NewBinding(
195 key.WithKeys("?"),
196 key.WithHelp("?", "help"),
197 ),
198}
199
200// In Update:
201case tea.KeyPressMsg:
202 switch {
203 case key.Matches(msg, keys.Quit):
204 return m, tea.Quit
205 }
206
207// For help integration, implement help.KeyMap interface:
208func (k KeyMap) ShortHelp() []key.Binding { return []key.Binding{k.Quit, k.Help} }
209func (k KeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{{k.Quit, k.Help}} }
210```
211
212### Composing Multiple Bubbles
213
214The list component is a good example of composition - it internally uses spinner, textinput, paginator, and help:
215
216```go
217type model struct {
218 list list.Model
219 viewport viewport.Model
220 help help.Model
221 spinner spinner.Model
222}
223```
224
225Combine their views with lipgloss layout:
226
227```go
228func (m model) View() string {
229 left := m.list.View()
230 right := m.viewport.View()
231 return lipgloss.JoinHorizontal(lipgloss.Top, left, right)
232}
233```
234
235### Window Size Handling
236
237Resize bubbles when the terminal size changes:
238
239```go
240case tea.WindowSizeMsg:
241 m.viewport.SetWidth(msg.Width)
242 m.viewport.SetHeight(msg.Height - headerHeight)
243 m.list.SetSize(msg.Width, msg.Height)
244 m.table.SetWidth(msg.Width)
245 m.table.SetHeight(msg.Height)
246 m.progress.SetWidth(msg.Width - padding)
247 m.help.SetWidth(msg.Width)
248```
249
250### ID-Based Message Routing
251
252Animated 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:
253
254```go
255m.spinner1, cmd1 = m.spinner1.Update(msg)
256m.spinner2, cmd2 = m.spinner2.Update(msg)
257```
258
259## Integration with Bubbletea and Lipgloss
260
261### Import Paths (v2)
262
263```go
264import (
265 tea "charm.land/bubbletea/v2"
266 "charm.land/bubbles/v2/spinner"
267 "charm.land/bubbles/v2/textinput"
268 "charm.land/lipgloss/v2"
269)
270```
271
272### Key Message Types
273
274Bubbles v2 uses `tea.KeyPressMsg` (not `tea.KeyMsg` from v1). Match with `key.Matches`:
275
276```go
277case tea.KeyPressMsg:
278 switch {
279 case key.Matches(msg, m.KeyMap.Up):
280 // handle up
281 }
282```
283
284### Progress Bar - Static vs Animated
285
286Progress supports two modes:
287
288```go
289// Animated: use SetPercent (returns cmd), Update processes FrameMsg
290cmd := m.progress.SetPercent(0.75)
291
292// Static: use ViewAs directly, no Update needed
293view := m.progress.ViewAs(0.75)
294```
295
296### List Item Interface
297
298The list component requires items to implement the `Item` interface:
299
300```go
301type Item interface {
302 FilterValue() string
303}
304```
305
306And a delegate implementing `ItemDelegate`:
307
308```go
309type ItemDelegate interface {
310 Render(w io.Writer, m Model, index int, item Item)
311 Height() int
312 Spacing() int
313 Update(msg tea.Msg, m *Model) tea.Cmd
314}
315```
316
317### Filepicker Selection
318
319Check for file selection in your Update:
320
321```go
322case tea.KeyPressMsg:
323 m.filepicker, cmd = m.filepicker.Update(msg)
324 if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
325 m.selectedFile = path
326 }
327```
328
329## Common Mistakes
330
3311. **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)`
332
3332. **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.
334
3353. **Not assigning Update result back.** Bubbles return value types (not pointers). `m.spinner.Update(msg)` without assignment discards the update.
336
3374. **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.
338
3395. **Using v1 message types.** In v2, it's `tea.KeyPressMsg` not `tea.KeyMsg`. Check the upgrade guide if migrating.
340
3416. **Not handling WindowSizeMsg.** Components with fixed dimensions (viewport, list, table, progress) need resizing or they clip/overflow.
342
3437. **Setting width/height to 0.** Viewport and table render empty strings when dimensions are 0. Always set dimensions before first render.
344
3458. **Calling Focus() without using the cmd.** `textinput.Focus()` returns a `tea.Cmd` for cursor blinking. If you drop it, the cursor won't blink.
346
347## Checklist
348
349- [ ] Embed bubble Model as a field in your model (not a pointer)
350- [ ] Call constructor with `New()` or `New(opts...)`
351- [ ] Return Init commands from your `Init()` (Tick for spinner, Init for timer/stopwatch)
352- [ ] Forward messages to bubble's `Update` and capture both return values
353- [ ] Collect commands with `tea.Batch` when using multiple bubbles
354- [ ] Call `Focus()` on interactive components and use returned cmd
355- [ ] Handle `tea.WindowSizeMsg` to resize dimension-aware components
356- [ ] Implement `Item` and `ItemDelegate` interfaces when using list
357- [ ] Use `key.Matches(msg, binding)` for key matching in v2
358- [ ] Style with lipgloss via Style fields or SetStyles methods