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

  1. Import Paths
  2. Global Patterns
  3. Per-Component Migration
  4. Light and Dark Styles
  5. 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 runeutil and memoization packages 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.KeyMsgtea.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. AdaptiveColorLightDark 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

v1v2
model.Blinkmodel.IsBlinked
model.BlinkCmd()model.Blink()

Filepicker

v1v2
DefaultStylesWithRenderer(r)DefaultStyles()
model.Height = 10model.SetHeight(10)
_ = model.Height_ = model.Height()

Help

v1v2
model.Width = 80model.SetWidth(80)
_ = model.Width_ = model.Width()
NewModel()New()

New functions:

  • DefaultStyles(isDark bool) Styles
  • DefaultDarkStyles() Styles
  • DefaultLightStyles() Styles

Apply styles explicitly:

// Before
h := help.New()
// Colors auto-adapted to terminal background

// After
h := help.New()
h.Styles = help.DefaultStyles(isDark)

List

v1v2
DefaultStyles()DefaultStyles(isDark)
NewDefaultItemStyles()NewDefaultItemStyles(isDark)
styles.FilterPromptstyles.Filter.Focused.Prompt / styles.Filter.Blurred.Prompt
styles.FilterCursorstyles.Filter.Cursor
NewModel(...)New(...)

The Styles.FilterPrompt and Styles.FilterCursor fields have been consolidated into Styles.Filter, which is a textinput.Styles struct.

Paginator

v1v2
DefaultKeyMap (var)DefaultKeyMap() (func)
model.UsePgUpPgDownKeysRemoved — customize KeyMap directly
model.UseLeftRightKeysRemoved — customize KeyMap directly
model.UseUpDownKeysRemoved — customize KeyMap directly
model.UseHLKeysRemoved — customize KeyMap directly
model.UseJKKeysRemoved — 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")))
v1v2
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 coloring
  • WithScaled(bool) — scale blend to filled portion

Spinner

v1v2
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))
v1v2
NewWithInterval(d)New(WithInterval(d))

Table

v1v2
model.viewport.Widthmodel.Width() / model.SetWidth(w)
model.viewport.Heightmodel.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)
v1v2
textarea.Style (type)textarea.StyleState (type)
model.FocusedStylemodel.Styles.Focused
model.BlurredStylemodel.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 offset
  • ScrollPosition() — returns scroll position
  • MoveToBeginning() / 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 Fieldv2 Location
Model.PromptStyleStyleState.Prompt
Model.TextStyleStyleState.Text
Model.PlaceholderStyleStyleState.Placeholder
Model.CompletionStyleStyleState.Suggestion
Model.CursorStyleStyles.Cursor
Model.Cursor (cursor.Model)Model.Cursor() (func → *tea.Cursor)

New:

  • Model.Styles() / Model.SetStyles(Styles) — get/set styles
  • Model.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)
v1v2
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 support
  • GetContent() string — retrieve content
  • FillHeight bool — fill viewport with empty lines
  • StyleLineFunc 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.

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 compat approach 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:

PackageRemovedReplacement
cursorModel.BlinkModel.IsBlinked
cursorModel.BlinkCmd()Model.Blink()
filepickerDefaultStylesWithRenderer(r)DefaultStyles()
filepickerModel.Height (field)Model.SetHeight() / Model.Height()
helpNewModelNew()
helpModel.Width (field)Model.SetWidth() / Model.Width()
listNewModelNew()
listDefaultStyles()DefaultStyles(isDark)
listNewDefaultItemStyles()NewDefaultItemStyles(isDark)
listStyles.FilterPromptStyles.Filter (textinput.Styles)
listStyles.FilterCursorStyles.Filter.Cursor
paginatorDefaultKeyMap (var)DefaultKeyMap() (func)
paginatorNewModelNew()
paginatorUsePgUpPgDownKeys etc.Customize KeyMap directly
progressWithGradient(a, b)WithColors(colors...)
progressWithDefaultGradient()WithDefaultBlend()
progressWithScaledGradient(a, b)WithColors(...) + WithScaled(true)
progressWithDefaultScaledGradient()WithDefaultBlend() + WithScaled(true)
progressWithSolidFill(string)WithColors(color)
progressWithColorProfile(p)Removed (automatic)
progressModel.Width (field)Model.SetWidth() / Model.Width()
spinnerNewModelNew()
spinnerTick() (package func)Model.Tick()
stopwatchNewWithInterval(d)New(WithInterval(d))
tableModel.Width (field)Model.SetWidth() / Model.Width()
tableModel.Height (field)Model.SetHeight() / Model.Height()
textareaDefaultKeyMap (var)DefaultKeyMap() (func)
textareaStyle (type)StyleState (type)
textareaModel.FocusedStyleModel.Styles.Focused
textareaModel.BlurredStyleModel.Styles.Blurred
textareaModel.SetCursor(col)Model.SetCursorColumn(col)
textareaDefaultStyles()DefaultStyles(isDark)
textinputDefaultKeyMap (var)DefaultKeyMap() (func)
textinputNewModelNew()
textinputModel.Width (field)Model.SetWidth() / Model.Width()
textinputModel.PromptStyleStyleState.Prompt
textinputModel.TextStyleStyleState.Text
textinputModel.PlaceholderStyleStyleState.Placeholder
textinputModel.CompletionStyleStyleState.Suggestion
textinputModel.CursorStyleStyles.Cursor
textinputModel.Cursor (cursor.Model)Model.Cursor() (func → *tea.Cursor)
timerNewWithInterval(t, i)New(t, WithInterval(i))
viewportNew(w, h int)New(...Option)
viewportModel.Width (field)Model.SetWidth() / Model.Width()
viewportModel.Height (field)Model.SetHeight() / Model.Height()
viewportModel.YOffset (field)Model.SetYOffset() / Model.YOffset()
viewportHighPerformanceRenderingRemoved
runeutilEntire packageMoved to internal/runeutil (not importable)

Part of Charm.

The Charm logo