charmbracelet/bubbletea
Declarative views
Active contributors: aymanbagabas, meowgorithm
Purpose
The defining feature of Bubble Tea v2 is the declarative View. Instead of imperatively toggling terminal modes via commands or startup options, a program returns a tea.View from its View() method that describes everything about the current frame: the content, alt screen state, mouse mode, cursor, colors, window title, progress bar, focus reporting, and keyboard enhancements.
The runtime diffs each frame against the previous one and emits exactly the ANSI escapes needed to take the terminal from the old state to the new state. There is no other way to change those settings.
The View struct
Defined in tea.go:
type View struct {
Content string
OnMouse func(msg MouseMsg) Cmd
Cursor *Cursor
BackgroundColor color.Color
ForegroundColor color.Color
WindowTitle string
ProgressBar *ProgressBar
AltScreen bool
ReportFocus bool
DisableBracketedPasteMode bool
MouseMode MouseMode
KeyboardEnhancements KeyboardEnhancements
}tea.NewView(s) is sugar that creates a View with Content = s. View.SetContent(s) is the same on an existing struct.
How the runtime applies it
graph LR
Update["Model.Update"] --> ModelView["Model.View()"]
ModelView -->|tea.View| Render["Program.render"]
Render --> Diff["cursedRenderer.flush<br/>diffs old vs new"]
Diff --> ANSI["ANSI mode toggles"]
Diff --> Cells["Cell buffer write"]
ANSI --> Out["TTY"]
Cells --> OutEvery flush:
- Compares the new
Viewtos.lastView. - For each scalar field that changed, emits the corresponding ANSI sequence (alt screen on/off, set/reset bracketed paste, set/reset focus reporting, mouse mode select, set window title, set foreground/background, set cursor color, set progress bar, set/reset Kitty keyboard flags).
- Re-runs the styled string through the cell buffer and writes the diff.
- Stores the new
Viewass.lastView.
The "set then reset" pairing is what makes close() safe — the renderer remembers what it turned on so it can turn it off when the program exits.
What changed in v2
In v1, you would have written:
p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseCellMotion())
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.SetWindowTitle("Hello")
}
func (m model) View() string { return "..." }In v2:
p := tea.NewProgram(model{})
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m model) View() tea.View {
v := tea.NewView("...")
v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion
v.WindowTitle = "Hello"
return v
}The full migration matrix lives in UPGRADE_GUIDE_V2.md. The short version: every "imperative knob" in v1 became a View field in v2.
Why this is better
- No state drift. It is impossible for the alt screen or mouse mode to be "stuck on" because of an unhandled error path. The view always reflects the truth.
- Easy diffing. Library authors writing reusable bubbles can return a
Viewwith just the fields they care about; the parent component composes them. - Easier testing. Tests assert that
Viewfields take the expected value rather than that a side-effecting command was issued. - Simpler suspend/resume. When a program is suspended and resumed, the renderer simply replays the last
Viewto bring the terminal back to the same state.
Composing views
The Content field is a single styled string. To compose multiple components, use Lip Gloss's lipgloss.JoinVertical, lipgloss.JoinHorizontal, or other layout helpers, then assign the joined string to Content. Mode flags from sub-components do not currently merge automatically — the parent decides which ones to surface.
View.OnMouse
The one slightly imperative escape hatch on View is OnMouse. It runs before the model sees a mouse message and can return a Cmd to translate a region-specific mouse event into a custom message. Use it sparingly — it can break the "single source of truth" if overused.
Related pages
- Cursed renderer — how
Viewbecomes ANSI bytes. - Reference / messages — every public
Msgtype. - Mouse, Keyboard — the input pieces of
View.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.