v2.0.0
Bubbles v2 is here! 🫧
We're thrilled to share Bubbles v2 with you! This release accompanies Bubble Tea v2 and Lip Gloss v2 and brings a ton of consistency, new features, and quality-of-life improvements across every component. Catch 'em all:
go get charm.land/bubbletea/v2
go get charm.land/bubbles/v2
go get charm.land/lipgloss/v2You can also check the Upgrade Guide for more info.
There are a lot of changes in here, but we've found upgrading pretty easy, especially with a linter. Read on for the full breakdown!
Note
When in doubt, check the examples for reference — they've all been updated for v2.
🏠 New Home
Bubbles v2 now lives at charm.land:
import "charm.land/bubbles/v2"All sub-packages follow the same pattern: charm.land/bubbles/v2/viewport, charm.land/bubbles/v2/list, etc.
🎨 Light and Dark Styles
Some Bubbles, like help, offer default styles for both light and dark backgrounds. Since Lip Gloss v2 removes AdaptiveColor, choosing light or dark is now a manual process. You've got a couple of options.
🎩 The Best Way
Have Bubble Tea query the background color for you. This properly queries the correct inputs and outputs, and happens in lockstep with your application:
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:
m.help.Styles = help.DefaultStyles(msg.IsDark())
return m, nil
}
// ...
}If you're using Wish you must do it this way to get the background color of the client.
🤠 The Quick Way
Use the compat package in Lip Gloss. It's less recommended because it contains blocking I/O that operates independently of Bubble Tea, and when used with Wish it won't detect the client's background:
import "charm.land/lipgloss/v2/compat"
var hasDarkBG = compat.HasDarkBackground()
h := help.New()
h.Styles = help.DefaultStyles(hasDarkBG)👀 Or Just Pick One
h.Styles = help.DefaultLightStyles() // light mode!
h.Styles = help.DefaultDarkStyles() // jk dark modeThis pattern applies to help, list, textarea, and textinput.
🔑 The Init You Know and Love is Back
After experimenting with a few different forms of Init during the alphas, we decided the v1 signature was the right call after all. Init was a bit too redundant for our tastes given that initialization already happens in New:
func (m Model) Init() tea.Cmd✨ The Big Highlights
Getters and Setters Everywhere
All components now use getter/setter methods instead of exported Width and Height fields. This lets us do internal bookkeeping when things change, and it makes the API consistent across every Bubble:
// Before
vp.Width = 40
fmt.Println(vp.Width)
// After
vp.SetWidth(40)
fmt.Println(vp.Width())Affected: filepicker, help, progress, table, textinput, viewport.
Functional Options
Constructors now use the functional options pattern instead of positional args or separate constructor functions:
vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
sw := stopwatch.New(stopwatch.WithInterval(500 * time.Millisecond))
t := timer.New(30*time.Second, timer.WithInterval(100*time.Millisecond))DefaultKeyMap is a Function Now
All DefaultKeyMap package-level variables are now functions, so you get fresh values every time:
km := textinput.DefaultKeyMap() // was textinput.DefaultKeyMap
km := textarea.DefaultKeyMap() // was textarea.DefaultKeyMap
km := paginator.DefaultKeyMap() // was paginator.DefaultKeyMapReal Cursor Support 🖱️
Both textarea and textinput now support real terminal cursors! The feature is opt-in, so by default your programs will continue to use the easy-breezy virtual cursor. Set VirtualCursor to false and use Model.Cursor() for the real deal. Check out the textarea and textinput examples to see it in action.
Cleaned House 🧹
All previously deprecated symbols have been removed:
NewModelvariants — just useNewspinner.Tick()— useModel.Tick()insteadpaginator.UsePgUpPgDownKeysand friends — customizeKeyMapdirectlyfilepicker.DefaultStylesWithRenderer()— Lip Gloss is pure now, useDefaultStyles()viewport.HighPerformanceRendering— no longer neededruneutilandmemoizationpackages moved tointernal/(they were never meant for public use anyway)
What's Changed: the Laundry List
🔮 Cursor
Model.Blinkrenamed toModel.IsBlinkedfor clarityModel.BlinkCmd()renamed toModel.Blink()- Each cursor now gets a unique ID
📂 Filepicker
DefaultStylesWithRenderer()removed — Lip Gloss is pure now, so just useDefaultStyles()Model.Heightbroken intoSetHeight(int)/Height() int
❓ Help
Model.Widthbroken intoSetWidth(int)/Width() int- New
DefaultStyles(isDark bool),DefaultDarkStyles(), andDefaultLightStyles() - Defaults to dark background styles out of the box
🥕 List
DefaultStyles()andNewDefaultItemStyles()now take anisDark boolparameterStyles.FilterPromptandStyles.FilterCursorhave been consolidated intoStyles.Filter(atextinput.Styles)GlobalIndexhelper added
📄 Paginator
DefaultKeyMapvariable →DefaultKeyMap()function- Deprecated fields (
UsePgUpPgDownKeys,UseLeftRightKeys, etc.) removed — customizeKeyMapdirectly
🌈 Progress
This one got the biggest makeover!
- Complete color API overhaul:
WithGradient/WithScaledGradient→WithColors(...color.Color)— pass 2+ colors for blendingWithSolidFill(string)→WithColors(color)— pass a single color for a solid fillWithDefaultGradient()→WithDefaultBlend()- New
WithScaled(bool)to scale the blend to fit only the filled portion - New
WithColorFunc(func(total, current float64) color.Color)for fully dynamic coloring WithColorProfileremoved — Bubble Tea handles this automatically now
Model.FullColorandModel.EmptyColorchanged fromstringtoimage/color.ColorModel.Widthbroken intoSetWidth(int)/Width() intModel.Updatenow returnsModelinstead oftea.Model- Improved blend algorithm with support for multiple color stops — special thanks to the legendary @lrstanley!
🌀 Spinner
Tick()package-level function removed — useModel.Tick()instead
⏱️ Stopwatch
NewWithInterval(d)removed — useNew(WithInterval(d))instead- Debounced tick messages
🔢 Table
Model.Width/Model.Heightreplaced with getter/setter methods- Uses
ansi.Truncateinstead ofrunewidth.Truncate - Fixed a critical out-of-bounds cursor bug — thanks @s0ders!
✏️ Textarea
The big change here is real cursor support — but that's opt-in, so by default your programs will keep using the virtual cursor.
DefaultKeyMapvariable →DefaultKeyMap()function- New
PageUp/PageDownkey bindings Model.FocusedStyle/Model.BlurredStyle→Model.Styles.Focused/Model.Styles.BlurredStyletype renamed toStyleState; newStylesstruct groupsFocused,Blurred, andCursorModel.SetCursorrenamed toModel.SetCursorColumnModel.Cursoris nowfunc() *tea.Cursorfor real cursor supportModel.VirtualCursorbool added — set tofalsewhen using a real cursor- New
DefaultStyles(isDark bool),DefaultDarkStyles(),DefaultLightStyles() - New methods:
Column(),ScrollYOffset(),ScrollPosition(),MoveToBeginning(),MoveToEnd() - Focus status now passed to
SetPromptFunc
📜 Textinput
Most of the changes here bring textinput to parity with textarea, including real cursor support. Styling has been consolidated into a Styles struct with Focused and Blurred states:
DefaultKeyMapvariable →DefaultKeyMap()functionModel.Widthbroken intoSetWidth(int)/Width() intModel.PromptStyle→StyleState.PromptModel.TextStyle→StyleState.TextModel.PlaceholderStyle→StyleState.PlaceholderModel.CompletionStyle→StyleState.SuggestionModel.Cursoris nowfunc() *tea.Cursorfor real cursor supportModel.VirtualCursor()/SetVirtualCursor(bool)addedModel.Styles()/SetStyles(Styles)added- New
DefaultStyles(isDark bool),DefaultDarkStyles(),DefaultLightStyles() - Exposed matched suggestions and suggestion index
⏲️ Timer
NewWithInterval(timeout, interval)removed — useNew(timeout, WithInterval(interval))- Debounced tick messages
📦 Viewport
viewport got a ton of love in v2. Let's dive in!
Breaking changes:
New(width, height int)→New(...Option)withWithWidth/WithHeightModel.Width,Model.Height,Model.YOffsetreplaced with getter/setter methodsHighPerformanceRenderingremoved
Shiny new features:
You can now scroll horizontally with the left and right arrow keys, and set up a custom gutter column for things like line numbers:
vp := viewport.New()
vp.SetContent("hello world")
// Show 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)
}Highlight parts of what's being viewed with regex:
vp.SetHighlights(regexp.MustCompile("hello").FindAllStringIndex(vp.GetContent(), -1))
vp.HighlightNext() // highlight and navigate to next match
vp.HighlightPrevious() // highlight and navigate to previous match
vp.ClearHighlights() // clear all highlightsLet viewport handle soft wrapping for you:
vp.SoftWrap = true
vp.SetContent("hello world from a very long line")Or, if you need fine control, use SetContentLines with "virtual lines" containing \n — they're treated as soft wraps automatically.
Also new:
- Horizontal mouse wheel scrolling (thanks @UnseenBook!)
GetContent()to retrieve contentFillHeightto pad the viewport with empty linesStyleLineFuncfor per-line stylingHighlightStyleandSelectedHighlightStylefor highlight appearance
Changelog
Fixed
- f744b92: fix(ci): use local golangci-lint config (@aymanbagabas)
- 251e612: fix(filepicker): fix a panic due to an unchecked assertion (#891) (@meowgorithm)
- f3f0ca0: fix(lint): exclude var-naming rule for revive (@aymanbagabas)
- d004225: fix(table): use
ansi.Truncateinstead ofrunewidth.Truncate(#884) (@jedevc) - 93a004a: fix(viewport): optimize subline splitting by skipping lines without line endings (@aymanbagabas)
- d016636: fix: changed 'recieve' to 'receive' for 100% quality of Go Report Card (#881) (@Atennop1)
- af98365: fix: lint issues (@aymanbagabas)
Docs
- c81d525: docs(readme): update for v2 (#888) (@aymanbagabas)
- 6a799f4: docs(readme): update header image, minor corrections (@meowgorithm)
- 24081b3: docs: add v2 upgrade and changes guide (#885) (@aymanbagabas)
- 3a5ea3e: docs: update mascot image (@aymanbagabas)
Other stuff
💝 That's a wrap!
Feel free to reach out, ask questions, give feedback, and let us know how it's going. We'd love to know what you think.
Part of Charm.
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة

