ANSI-aware overlay painter for Bubble Tea / lipgloss views.
Go Version Go Reference GitHub release (latest by date) CI License: MIT
bubble-overlay paints rectangular blocks of styled text on top of an
existing ANSI-styled string at a given (row, col) cell position. It's the
missing primitive for modals, popups, tooltips, filepickers, and floating
panels in Bubble Tea /
lipgloss apps — render your
base view as a single string, render your popup as another, then composite.
- SGR-safe. Existing ANSI styles around the overlaid region are preserved; cells underneath are replaced. The overlay is terminated with
\x1b[0mso its styles don't bleed into the row's tail. - Cell-accurate. Uses
charmbracelet/x/ansifor width — wide-character / emoji handling matches what the terminal actually renders, not byte counts. - Auto-grow. Overlays that extend past the bottom of the base string append new lines instead of truncating.
- Auto-pad. Overlays past the right edge of a short row are padded with spaces, so a popup on row 3 column 40 still lands correctly when the base row is only 10 cells wide.
- Tiny. Two functions. No state. Drop-in.
go get github.com/floatpane/bubble-overlay
Requires Go 1.26+.
package main import ( "fmt" "github.com/charmbracelet/lipgloss" "github.com/floatpane/bubble-overlay" ) func main() { base := lipgloss.NewStyle(). Foreground(lipgloss.Color("240")). Render("a quiet inbox view\nwith two lines\nand a third") popup := lipgloss.NewStyle(). Background(lipgloss.Color("57")). Foreground(lipgloss.Color("231")). Padding(0, 1). Render("are you sure?\nyes / no") block := strings.Split(popup, "\n") fmt.Println(overlay.Block(base, block, 1, 4)) }
For the common case of a centered modal — command palette, confirmation dialog,
tooltip — Center handles the positioning automatically:
composited := overlay.Center(baseView, popupView, termWidth, termHeight)
It computes the centered (row, col) from the popup's visual size and your
screen dimensions, then calls Block. Rows/columns clamp to zero if the popup
is larger than the screen.
// Center places popup as a floating layer centered over base within a screen // of screenW ×ばつ screenH cells. func Center(base, popup string, screenW, screenH int) string // Paint a multi-line block on top of base at (row, col). func Block(base string, block []string, row, col int) string // Paint a single overlay line on top of base at col. func Line(base, overlay string, col int) string
You have a Bubble Tea View() returning a styled multi-line string and
you want to render a modal/popup over it without:
- Re-rendering the base view with a "modal-shaped hole" cut out of it.
- Walking the ANSI sequences yourself.
- Truncating styles that span across the modal region.
bubble-overlay does the composite for you. The base view stays a single
string; the modal stays a single string; you call Block and emit the
result.
Full API reference: pkg.go.dev/github.com/floatpane/bubble-overlay
PRs welcome. See CONTRIBUTING.md.
Report vulnerabilities privately via SECURITY.md.
MIT. See LICENSE.