Open-Source Wikis

/

Bubble Tea

/

Features

/

Declarative views

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 --> Out

Every flush:

  1. Compares the new View to s.lastView.
  2. 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).
  3. Re-runs the styled string through the cell buffer and writes the diff.
  4. Stores the new View as s.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 View with just the fields they care about; the parent component composes them.
  • Easier testing. Tests assert that View fields 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 View to 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.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Declarative views – Bubble Tea wiki | Factory