Upgrading to Bubbles v2
February 17, 2026 · View on GitHub
This guide covers every breaking change when migrating from Bubbles v1 (github.com/charmbracelet/bubbles) to Bubbles v2 (charm.land/bubbles/v2). It is written for both humans and LLM-assisted migration tools.
Companion upgrades required. Bubbles v2 requires Bubble Tea v2 and Lip Gloss v2. Upgrade all three together:
go get charm.land/bubbletea/v2@latest go get charm.land/bubbles/v2@latest go get charm.land/lipgloss/v2@latest
Table of Contents
- Import Paths
- Global Patterns
- Per-Component Migration
- Light and Dark Styles
- Removed Symbols Reference
1. Import Paths
Replace all github.com/charmbracelet/bubbles imports with charm.land/bubbles/v2:
// Before
import (
"github.com/charmbracelet/bubbles/cursor"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/paginator"
"github.com/charmbracelet/bubbles/progress"
"github.com/charmbracelet/bubbles/runeutil"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/stopwatch"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/timer"
"github.com/charmbracelet/bubbles/viewport"
)
// After
import (
"charm.land/bubbles/v2/cursor"
"charm.land/bubbles/v2/help"
"charm.land/bubbles/v2/key"
"charm.land/bubbles/v2/list"
"charm.land/bubbles/v2/paginator"
"charm.land/bubbles/v2/progress"
"charm.land/bubbles/v2/spinner"
"charm.land/bubbles/v2/stopwatch"
"charm.land/bubbles/v2/table"
"charm.land/bubbles/v2/textarea"
"charm.land/bubbles/v2/textinput"
"charm.land/bubbles/v2/timer"
"charm.land/bubbles/v2/viewport"
)
Note: The
runeutilandmemoizationpackages are now internal and no longer importable.
Search-and-replace pattern:
github.com/charmbracelet/bubbles/ → charm.land/bubbles/v2/
github.com/charmbracelet/bubbles → charm.land/bubbles/v2
2. Global Patterns
These patterns repeat across multiple components. Address them first for the broadest impact.
2a. tea.KeyMsg → tea.KeyPressMsg
Bubble Tea v2 renames tea.KeyMsg to tea.KeyPressMsg. All Bubbles that handle key events have been updated. Update your own Update functions:
// Before
case tea.KeyMsg:
// After
case tea.KeyPressMsg:
2b. Exported Width/Height Fields → Getter/Setter Methods
Many components replaced exported Width and Height fields with methods. The general pattern:
// Before
m.Width = 40
m.Height = 20
fmt.Println(m.Width, m.Height)
// After
m.SetWidth(40)
m.SetHeight(20)
fmt.Println(m.Width(), m.Height())
Affected components: filepicker, help, progress, table, textinput, viewport.
2c. DefaultKeyMap Variables → Functions
Global mutable DefaultKeyMap variables are now functions returning fresh values:
// Before
km := textinput.DefaultKeyMap
km.Paste.SetEnabled(false)
// After
km := textinput.DefaultKeyMap()
km.Paste.SetEnabled(false)
Affected components: paginator, textarea, textinput.
2d. AdaptiveColor → LightDark with isDark bool
Lip Gloss v2 removes AdaptiveColor. Style functions that previously auto-adapted now require an explicit isDark bool parameter. See Section 4 for the full pattern.
2e. Removed NewModel Aliases
All NewModel variables (deprecated aliases for New) have been removed. Use New directly.
Affected components: help, list, paginator, spinner, textinput.
3. Per-Component Migration
Cursor
| v1 | v2 |
|---|---|
model.Blink | model.IsBlinked |
model.BlinkCmd() | model.Blink() |
Filepicker
| v1 | v2 |
|---|---|
DefaultStylesWithRenderer(r) | DefaultStyles() |
model.Height = 10 | model.SetHeight(10) |
_ = model.Height | _ = model.Height() |
Help
| v1 | v2 |
|---|---|
model.Width = 80 | model.SetWidth(80) |
_ = model.Width | _ = model.Width() |
NewModel() | New() |
New functions:
DefaultStyles(isDark bool) StylesDefaultDarkStyles() StylesDefaultLightStyles() Styles
Apply styles explicitly:
// Before
h := help.New()
// Colors auto-adapted to terminal background
// After
h := help.New()
h.Styles = help.DefaultStyles(isDark)
List
| v1 | v2 |
|---|---|
DefaultStyles() | DefaultStyles(isDark) |
NewDefaultItemStyles() | NewDefaultItemStyles(isDark) |
styles.FilterPrompt | styles.Filter.Focused.Prompt / styles.Filter.Blurred.Prompt |
styles.FilterCursor | styles.Filter.Cursor |
NewModel(...) | New(...) |
The Styles.FilterPrompt and Styles.FilterCursor fields have been consolidated into Styles.Filter, which is a textinput.Styles struct.
Paginator
| v1 | v2 |
|---|---|
DefaultKeyMap (var) | DefaultKeyMap() (func) |
model.UsePgUpPgDownKeys | Removed — customize KeyMap directly |
model.UseLeftRightKeys | Removed — customize KeyMap directly |
model.UseUpDownKeys | Removed — customize KeyMap directly |
model.UseHLKeys | Removed — customize KeyMap directly |
model.UseJKKeys | Removed — customize KeyMap directly |
NewModel(...) | New(...) |
Progress
This component has the most extensive changes.
Width
// Before
p.Width = 40
fmt.Println(p.Width)
// After
p.SetWidth(40)
fmt.Println(p.Width())
Colors
Color types changed from string to image/color.Color:
// Before
p.FullColor = "#FF0000"
p.EmptyColor = "#333333"
// After
p.FullColor = lipgloss.Color("#FF0000")
p.EmptyColor = lipgloss.Color("#333333")
Gradient/Blend Options
// Before
progress.New(progress.WithGradient("#5A56E0", "#EE6FF8"))
progress.New(progress.WithDefaultGradient())
progress.New(progress.WithScaledGradient("#5A56E0", "#EE6FF8"))
progress.New(progress.WithDefaultScaledGradient())
progress.New(progress.WithSolidFill("#7571F9"))
// After
progress.New(progress.WithColors(lipgloss.Color("#5A56E0"), lipgloss.Color("#EE6FF8")))
progress.New(progress.WithDefaultBlend())
progress.New(progress.WithColors(lipgloss.Color("#5A56E0"), lipgloss.Color("#EE6FF8")), progress.WithScaled(true))
progress.New(progress.WithDefaultBlend(), progress.WithScaled(true))
progress.New(progress.WithColors(lipgloss.Color("#7571F9")))
| v1 | v2 |
|---|---|
WithGradient(a, b string) | WithColors(colors ...color.Color) |
WithDefaultGradient() | WithDefaultBlend() |
WithScaledGradient(a, b string) | WithColors(...) + WithScaled(true) |
WithDefaultScaledGradient() | WithDefaultBlend() + WithScaled(true) |
WithSolidFill(string) | WithColors(color) (single color) |
WithColorProfile(termenv.Profile) | Removed (automatic) |
Update() (tea.Model, tea.Cmd) | Update() (Model, tea.Cmd) |
New options:
WithColorFunc(func(total, current float64) color.Color)— dynamic per-cell coloringWithScaled(bool)— scale blend to filled portion
Spinner
| v1 | v2 |
|---|---|
NewModel() | New() |
spinner.Tick() (package func) | model.Tick() (method) |
Stopwatch
// Before
sw := stopwatch.NewWithInterval(500 * time.Millisecond)
// After
sw := stopwatch.New(stopwatch.WithInterval(500 * time.Millisecond))
| v1 | v2 |
|---|---|
NewWithInterval(d) | New(WithInterval(d)) |
Table
| v1 | v2 |
|---|---|
model.viewport.Width | model.Width() / model.SetWidth(w) |
model.viewport.Height | model.Height() / model.SetHeight(h) |
The table already had SetWidth/SetHeight/Width()/Height() in v1, but internally these now use viewport getter/setters.
Textarea
KeyMap
// Before
km := textarea.DefaultKeyMap
// After
km := textarea.DefaultKeyMap()
New key bindings added: PageUp, PageDown.
Styles
The styling system has been restructured:
// Before
ta := textarea.New()
ta.FocusedStyle.Base = lipgloss.NewStyle().Border(lipgloss.RoundedBorder())
ta.BlurredStyle.Base = lipgloss.NewStyle().Border(lipgloss.HiddenBorder())
// After
ta := textarea.New()
// Styles are now nested under a Styles struct
// Access via Styles.Focused and Styles.Blurred (type StyleState)
| v1 | v2 |
|---|---|
textarea.Style (type) | textarea.StyleState (type) |
model.FocusedStyle | model.Styles.Focused |
model.BlurredStyle | model.Styles.Blurred |
DefaultStyles() (focused, blurred Style) | DefaultStyles(isDark bool) Styles |
Cursor
// Before
ta.Cursor // cursor.Model (virtual cursor)
ta.SetCursor(col) // set cursor column
// After
ta.Cursor() // func() *tea.Cursor (real cursor)
ta.SetCursorColumn(col) // renamed for clarity
ta.VirtualCursor // bool: true = virtual, false = real
ta.Styles.Cursor // CursorStyle for cursor appearance
New additions:
Column()— returns current cursor column (0-indexed)ScrollYOffset()— returns vertical scroll offsetScrollPosition()— returns scroll positionMoveToBeginning()/MoveToEnd()— navigate to start/end
Textinput
KeyMap
// Before
km := textinput.DefaultKeyMap
// After
km := textinput.DefaultKeyMap()
Width
// Before
ti.Width = 40
// After
ti.SetWidth(40)
Styles
Individual style fields have moved into a Styles struct:
// Before
ti.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
ti.TextStyle = lipgloss.NewStyle()
ti.PlaceholderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
ti.CompletionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
// After
s := textinput.DefaultStyles(isDark)
s.Focused.Prompt = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
s.Focused.Text = lipgloss.NewStyle()
s.Focused.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
s.Focused.Suggestion = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
ti.SetStyles(s)
| v1 Field | v2 Location |
|---|---|
Model.PromptStyle | StyleState.Prompt |
Model.TextStyle | StyleState.Text |
Model.PlaceholderStyle | StyleState.Placeholder |
Model.CompletionStyle | StyleState.Suggestion |
Model.CursorStyle | Styles.Cursor |
Model.Cursor (cursor.Model) | Model.Cursor() (func → *tea.Cursor) |
New:
Model.Styles()/Model.SetStyles(Styles)— get/set stylesModel.VirtualCursor()/Model.SetVirtualCursor(bool)— toggle cursor mode
Timer
// Before
t := timer.NewWithInterval(30*time.Second, 100*time.Millisecond)
t := timer.New(30 * time.Second)
// After
t := timer.New(30*time.Second, timer.WithInterval(100*time.Millisecond))
t := timer.New(30 * time.Second)
| v1 | v2 |
|---|---|
NewWithInterval(timeout, interval) | New(timeout, WithInterval(interval)) |
Viewport
This component has the most new features alongside its breaking changes.
Constructor
// Before
vp := viewport.New(80, 24)
// After
vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
// or
vp := viewport.New()
vp.SetWidth(80)
vp.SetHeight(24)
Width, Height, YOffset
// Before
vp.Width = 80
vp.Height = 24
vp.YOffset = 5
fmt.Println(vp.Width, vp.Height, vp.YOffset)
// After
vp.SetWidth(80)
vp.SetHeight(24)
vp.SetYOffset(5)
fmt.Println(vp.Width(), vp.Height(), vp.YOffset())
Removed
HighPerformanceRendering— removed entirely (deprecated in Bubble Tea v2)
New Features (non-breaking)
These are additions you can adopt incrementally:
- Soft wrapping:
vp.SoftWrap = true - Left gutter for line numbers:
vp.LeftGutterFunc = func(info viewport.GutterContext) string { if info.Soft { return " │ " } if info.Index >= info.TotalLines { return " ~ │ " } return fmt.Sprintf("%4d │ ", info.Index+1) } - Highlighting:
vp.SetHighlights(regexp.MustCompile("pattern").FindAllStringIndex(vp.GetContent(), -1)) vp.HighlightNext() vp.HighlightPrevious() vp.ClearHighlights() SetContentLines([]string)— set lines directly with virtual soft-wrap supportGetContent() string— retrieve contentFillHeight bool— fill viewport with empty linesStyleLineFunc func(int) lipgloss.Style— per-line styling- Horizontal scrolling with left/right arrow keys
- Horizontal mouse wheel scrolling
4. Light and Dark Styles
Lip Gloss v2 removes AdaptiveColor, so Bubbles no longer auto-detect terminal background. You must explicitly choose light or dark styles.
Recommended: Query via Bubble Tea
func (m model) Init() tea.Cmd {
return tea.RequestBackgroundColor
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.BackgroundColorMsg:
isDark := msg.IsDark()
m.help.Styles = help.DefaultStyles(isDark)
m.list.Styles = list.DefaultStyles(isDark)
// ... apply to other components
}
return m, nil
}
This is required when using Wish to detect the client's background.
Quick: Use compat Package
import "charm.land/lipgloss/v2/compat"
var isDark = compat.HasDarkBackground()
func main() {
h := help.New()
h.Styles = help.DefaultStyles(isDark)
}
Warning: The
compatapproach uses blocking I/O outside Bubble Tea's event loop and will not detect remote client backgrounds over SSH.
Manual
h.Styles = help.DefaultDarkStyles() // force dark
h.Styles = help.DefaultLightStyles() // force light
5. Removed Symbols Reference
Quick-reference table of all removed symbols and their replacements:
| Package | Removed | Replacement |
|---|---|---|
cursor | Model.Blink | Model.IsBlinked |
cursor | Model.BlinkCmd() | Model.Blink() |
filepicker | DefaultStylesWithRenderer(r) | DefaultStyles() |
filepicker | Model.Height (field) | Model.SetHeight() / Model.Height() |
help | NewModel | New() |
help | Model.Width (field) | Model.SetWidth() / Model.Width() |
list | NewModel | New() |
list | DefaultStyles() | DefaultStyles(isDark) |
list | NewDefaultItemStyles() | NewDefaultItemStyles(isDark) |
list | Styles.FilterPrompt | Styles.Filter (textinput.Styles) |
list | Styles.FilterCursor | Styles.Filter.Cursor |
paginator | DefaultKeyMap (var) | DefaultKeyMap() (func) |
paginator | NewModel | New() |
paginator | UsePgUpPgDownKeys etc. | Customize KeyMap directly |
progress | WithGradient(a, b) | WithColors(colors...) |
progress | WithDefaultGradient() | WithDefaultBlend() |
progress | WithScaledGradient(a, b) | WithColors(...) + WithScaled(true) |
progress | WithDefaultScaledGradient() | WithDefaultBlend() + WithScaled(true) |
progress | WithSolidFill(string) | WithColors(color) |
progress | WithColorProfile(p) | Removed (automatic) |
progress | Model.Width (field) | Model.SetWidth() / Model.Width() |
spinner | NewModel | New() |
spinner | Tick() (package func) | Model.Tick() |
stopwatch | NewWithInterval(d) | New(WithInterval(d)) |
table | Model.Width (field) | Model.SetWidth() / Model.Width() |
table | Model.Height (field) | Model.SetHeight() / Model.Height() |
textarea | DefaultKeyMap (var) | DefaultKeyMap() (func) |
textarea | Style (type) | StyleState (type) |
textarea | Model.FocusedStyle | Model.Styles.Focused |
textarea | Model.BlurredStyle | Model.Styles.Blurred |
textarea | Model.SetCursor(col) | Model.SetCursorColumn(col) |
textarea | DefaultStyles() | DefaultStyles(isDark) |
textinput | DefaultKeyMap (var) | DefaultKeyMap() (func) |
textinput | NewModel | New() |
textinput | Model.Width (field) | Model.SetWidth() / Model.Width() |
textinput | Model.PromptStyle | StyleState.Prompt |
textinput | Model.TextStyle | StyleState.Text |
textinput | Model.PlaceholderStyle | StyleState.Placeholder |
textinput | Model.CompletionStyle | StyleState.Suggestion |
textinput | Model.CursorStyle | Styles.Cursor |
textinput | Model.Cursor (cursor.Model) | Model.Cursor() (func → *tea.Cursor) |
timer | NewWithInterval(t, i) | New(t, WithInterval(i)) |
viewport | New(w, h int) | New(...Option) |
viewport | Model.Width (field) | Model.SetWidth() / Model.Width() |
viewport | Model.Height (field) | Model.SetHeight() / Model.Height() |
viewport | Model.YOffset (field) | Model.SetYOffset() / Model.YOffset() |
viewport | HighPerformanceRendering | Removed |
runeutil | Entire package | Moved to internal/runeutil (not importable) |
Part of Charm.
