Skip to main content
We don’t take API changes lightly and strive to make the upgrade process as simple as possible. If something feels way off, let us know.

Migration Checklist

Here’s the short version — a checklist you can follow top to bottom. Each item links to the relevant section below.
1

Update import paths

Change module paths to the new vanity domain
2

Update View method signature

Change View() string to View() tea.View
3

Update key message handling

Replace tea.KeyMsg with tea.KeyPressMsg
4

Update key fields

Change msg.Type to msg.Code, msg.Runes to msg.Text, etc.
5

Fix space key handling

Replace case " ": with case "space":
6

Update mouse messages

Update mouse message types and method calls
7

Rename mouse button constants

Update button constant names
8

Remove old program options

Replace with View fields
9

Remove imperative commands

Replace with View fields
10

Remove old program methods

Update to use View fields or new methods
11

Rename APIs

Update tea.WindowSize() and tea.Sequentially()

Import Paths

The module path changed to a vanity domain. Lip Gloss moved too.
Before
import tea "github.com/charmbracelet/bubbletea"
import "github.com/charmbracelet/lipgloss"
After
import tea "charm.land/bubbletea/v2"
import "charm.land/lipgloss/v2"

The Big Idea: Declarative Views

The single biggest change in v2 is the shift from imperative commands to declarative View fields.
In v1, you’d use program options like tea.WithAltScreen() and commands like tea.EnterAltScreen to toggle terminal features on and off. In v2, you just set fields on the tea.View struct in your View() method and Bubble Tea handles the rest. This means: no more startup option flags, no more toggle commands, no more fighting over state. Just declare what you want and Bubble Tea will make it so.
v1: imperative — scattered across NewProgram, Init, and Update
p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseCellMotion())
v2: declarative — everything lives in View()
func (m model) View() tea.View {
    v := tea.NewView("Hello!")
    v.AltScreen = true
    v.MouseMode = tea.MouseModeCellMotion
    return v
}
Keep this in mind as you go through the rest of the guide — most of the “removed” things simply moved into View fields.

View Returns a tea.View Now

The View() method no longer returns a string. It returns a tea.View struct.
Before
func (m model) View() string {
    return "Hello, world!"
}
After
func (m model) View() tea.View {
    return tea.NewView("Hello, world!")
}
You can also use the longer form if you need to set additional fields:
Extended form
func (m model) View() tea.View {
    var v tea.View
    v.SetContent("Hello, world!")
    v.AltScreen = true
    return v
}

Available View Fields

The tea.View struct has fields for everything that used to be controlled by options and commands:
View FieldWhat It Does
ContentThe rendered string (set via SetContent() or NewView())
AltScreenEnter/exit the alternate screen buffer
MouseModeMouseModeNone, MouseModeCellMotion, or MouseModeAllMotion
ReportFocusEnable focus/blur event reporting
DisableBracketedPasteModeDisable bracketed paste
WindowTitleSet the terminal window title
CursorControl cursor position, shape, color, and blink
ForegroundColorSet the terminal foreground color
BackgroundColorSet the terminal background color
ProgressBarShow a native terminal progress bar
KeyboardEnhancementsRequest keyboard enhancement features
OnMouseIntercept mouse messages based on view content

Key Messages

Key messages got a major overhaul. Here’s the quick rundown:

tea.KeyMsg is now an interface

In v1, tea.KeyMsg was a struct you’d match on for key presses. In v2, it’s an interface that covers both key presses and releases.
For most code, you want tea.KeyPressMsg:
Before
case tea.KeyMsg:
    switch msg.String() {
    case "q":
        return m, tea.Quit
    }
After
case tea.KeyPressMsg:
    switch msg.String() {
    case "q":
        return m, tea.Quit
    }
If you want to handle both presses and releases, use tea.KeyMsg and type-switch inside:
Handling both presses and releases
case tea.KeyMsg:
    switch key := msg.(type) {
    case tea.KeyPressMsg:
        // key press
    case tea.KeyReleaseMsg:
        // key release
    }

Key fields changed

v1v2Notes
msg.Typemsg.CodeA rune — can be tea.KeyEnter, 'a', etc.
msg.Runesmsg.TextNow a string, not []rune
msg.Altmsg.Modmsg.Mod.Contains(tea.ModAlt) for alt, etc.
tea.KeyRuneCheck len(msg.Text) > 0 instead
tea.KeyCtrlCUse msg.String() == "ctrl+c" or check msg.Code + msg.Mod

Space bar changed

Space bar now returns "space" instead of " " when using msg.String():
Before
case " ":
After
case "space":
key.Code is still ' ' and key.Text is still " ", but String() returns "space".

Ctrl+key matching

Before
case tea.KeyCtrlC:
    // ctrl+c
After (option A — string matching)
case tea.KeyPressMsg:
    switch msg.String() {
    case "ctrl+c":
        // ctrl+c
    }
After (option B — field matching)
case tea.KeyPressMsg:
    if msg.Code == 'c' && msg.Mod == tea.ModCtrl {
        // ctrl+c
    }

New Key fields

These are new in v2 and don’t have v1 equivalents:
  • key.ShiftedCode — the shifted key code (e.g., 'B' when pressing shift+b)
  • key.BaseCode — the key on a US PC-101 layout (handy for international keyboards)
  • key.IsRepeat — whether the key is auto-repeating (Kitty protocol / Windows Console only)
  • key.Keystroke() — like String() but always includes modifier info

Paste Messages

Paste events no longer come in as tea.KeyMsg with a Paste flag. They’re now their own message types:
Before
case tea.KeyMsg:
    if msg.Paste {
        m.text += string(msg.Runes)
    }
After
case tea.PasteMsg:
    m.text += msg.Content
case tea.PasteStartMsg:
    // paste started
case tea.PasteEndMsg:
    // paste ended

Mouse Messages

tea.MouseMsg is now an interface

In v1, tea.MouseMsg was a struct with X, Y, Button, etc. In v2, it’s an interface.
You get the coordinates by calling msg.Mouse():
Before
case tea.MouseMsg:
    x, y := msg.X, msg.Y
After
case tea.MouseMsg:
    mouse := msg.Mouse()
    x, y := mouse.X, mouse.Y

Mouse events are split by type

Instead of checking msg.Action, match on specific message types:
Before
case tea.MouseMsg:
    if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
        // left click
    }
After
case tea.MouseClickMsg:
    if msg.Button == tea.MouseLeft {
        // left click
    }
case tea.MouseReleaseMsg:
    // release
case tea.MouseWheelMsg:
    // scroll
case tea.MouseMotionMsg:
    // movement

Button constants renamed

v1v2
tea.MouseButtonLefttea.MouseLeft
tea.MouseButtonRighttea.MouseRight
tea.MouseButtonMiddletea.MouseMiddle
tea.MouseButtonWheelUptea.MouseWheelUp
tea.MouseButtonWheelDowntea.MouseWheelDown
tea.MouseButtonWheelLefttea.MouseWheelLeft
tea.MouseButtonWheelRighttea.MouseWheelRight

tea.MouseEventtea.Mouse

The MouseEvent struct is gone. The new Mouse struct has X, Y, Button, and Mod fields.

Mouse mode is now a View field

Before
p := tea.NewProgram(model{}, tea.WithMouseCellMotion())
After
func (m model) View() tea.View {
    v := tea.NewView("...")
    v.MouseMode = tea.MouseModeCellMotion
    return v
}

Removed Program Options

These options no longer exist. They all moved to View fields.
Removed OptionDo This Instead
tea.WithAltScreen()view.AltScreen = true
tea.WithMouseCellMotion()view.MouseMode = tea.MouseModeCellMotion
tea.WithMouseAllMotion()view.MouseMode = tea.MouseModeAllMotion
tea.WithReportFocus()view.ReportFocus = true
tea.WithoutBracketedPaste()view.DisableBracketedPasteMode = true
tea.WithInputTTY()Just remove it — v2 always opens the TTY for input automatically
tea.WithANSICompressor()Just remove it — the new renderer handles optimization automatically

Removed Commands

These commands no longer exist. Set the corresponding View field instead.
Removed CommandDo This Instead
tea.EnterAltScreenview.AltScreen = true
tea.ExitAltScreenview.AltScreen = false
tea.EnableMouseCellMotionview.MouseMode = tea.MouseModeCellMotion
tea.EnableMouseAllMotionview.MouseMode = tea.MouseModeAllMotion
tea.DisableMouseview.MouseMode = tea.MouseModeNone
tea.HideCursorview.Cursor = nil
tea.ShowCursorview.Cursor = &tea.Cursor{...} or tea.NewCursor(x, y)
tea.EnableBracketedPasteview.DisableBracketedPasteMode = false
tea.DisableBracketedPasteview.DisableBracketedPasteMode = true
tea.EnableReportFocusview.ReportFocus = true
tea.DisableReportFocusview.ReportFocus = false
tea.SetWindowTitle("...")view.WindowTitle = "..."

Removed Program Methods

These methods on *Program are gone.
Removed MethodDo This Instead
p.Start()p.Run()
p.StartReturningModel()p.Run()
p.EnterAltScreen()view.AltScreen = true in View()
p.ExitAltScreen()view.AltScreen = false in View()
p.EnableMouseCellMotion()view.MouseMode in View()
p.DisableMouseCellMotion()view.MouseMode = tea.MouseModeNone in View()
p.EnableMouseAllMotion()view.MouseMode in View()
p.DisableMouseAllMotion()view.MouseMode = tea.MouseModeNone in View()
p.SetWindowTitle(...)view.WindowTitle in View()

Renamed APIs

v1v2Notes
tea.Sequentially(...)tea.Sequence(...)Sequentially was already deprecated in v1
tea.WindowSize()tea.RequestWindowSizeNow returns Msg directly, not a Cmd

New Program Options

These are new in v2:
OptionWhat It Does
tea.WithColorProfile(p)Force a specific color profile (great for testing)
tea.WithWindowSize(w, h)Set initial terminal size (great for testing)

Complete Before & After

Here’s a minimal but complete program showing the most common migration patterns side by side.

v1 Example

v1
package main

import (
    "fmt"
    "os"

    tea "github.com/charmbracelet/bubbletea"
)

type model struct {
    count int
}

func (m model) Init() tea.Cmd {
    return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "q", "ctrl+c":
            return m, tea.Quit
        case " ":
            m.count++
        }
    case tea.MouseMsg:
        if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
            m.count++
        }
    }
    return m, nil
}

func (m model) View() string {
    return fmt.Sprintf("Count: %d\n\nSpace or click to increment. q to quit.\n", m.count)
}

func main() {
    p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseCellMotion())
    if _, err := p.Run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

v2 Example

v2
package main

import (
    "fmt"
    "os"

    tea "charm.land/bubbletea/v2"
)

type model struct {
    count int
}

func (m model) Init() tea.Cmd {
    return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch msg.String() {
        case "q", "ctrl+c":
            return m, tea.Quit
        case "space":
            m.count++
        }
    case tea.MouseClickMsg:
        if msg.Button == tea.MouseLeft {
            m.count++
        }
    }
    return m, nil
}

func (m model) View() tea.View {
    v := tea.NewView(fmt.Sprintf("Count: %d\n\nSpace or click to increment. q to quit.\n", m.count))
    v.AltScreen = true
    v.MouseMode = tea.MouseModeCellMotion
    return v
}

func main() {
    p := tea.NewProgram(model{})
    if _, err := p.Run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Notice how the NewProgram call got simpler? All the terminal feature flags moved into View() where they belong.

Quick Reference

A flat old → new lookup table. Handy for search-and-replace and LLM-assisted migration.

Import Paths

v1v2
github.com/charmbracelet/bubbleteacharm.land/bubbletea/v2
github.com/charmbracelet/lipglosscharm.land/lipgloss/v2

Model Interface

v1v2
View() stringView() tea.View

Key Events

v1v2
tea.KeyMsg (struct)tea.KeyPressMsg for presses, tea.KeyMsg (interface) for both
msg.Typemsg.Code
msg.Runesmsg.Text (string, not []rune)
msg.Altmsg.Mod.Contains(tea.ModAlt)
tea.KeyRunecheck len(msg.Text) > 0
tea.KeyCtrlCmsg.Code == 'c' && msg.Mod == tea.ModCtrl or msg.String() == "ctrl+c"
case " ": (space)case "space":

Mouse Events

v1v2
tea.MouseMsg (struct)tea.MouseMsg (interface) — call .Mouse() for the data
tea.MouseEventtea.Mouse
tea.MouseButtonLefttea.MouseLeft
tea.MouseButtonRighttea.MouseRight
tea.MouseButtonMiddletea.MouseMiddle
tea.MouseButtonWheelUptea.MouseWheelUp
tea.MouseButtonWheelDowntea.MouseWheelDown
msg.X, msg.Y (direct)msg.Mouse().X, msg.Mouse().Y

Options → View Fields

v1 Optionv2 View Field
tea.WithAltScreen()view.AltScreen = true
tea.WithMouseCellMotion()view.MouseMode = tea.MouseModeCellMotion
tea.WithMouseAllMotion()view.MouseMode = tea.MouseModeAllMotion
tea.WithReportFocus()view.ReportFocus = true
tea.WithoutBracketedPaste()view.DisableBracketedPasteMode = true

Commands → View Fields

v1 Commandv2 View Field
tea.EnterAltScreen / tea.ExitAltScreenview.AltScreen = true/false
tea.EnableMouseCellMotionview.MouseMode = tea.MouseModeCellMotion
tea.EnableMouseAllMotionview.MouseMode = tea.MouseModeAllMotion
tea.DisableMouseview.MouseMode = tea.MouseModeNone
tea.HideCursor / tea.ShowCursorview.Cursor = nil / view.Cursor = &tea.Cursor{...}
tea.EnableBracketedPaste / tea.DisableBracketedPasteview.DisableBracketedPasteMode = false/true
tea.EnableReportFocus / tea.DisableReportFocusview.ReportFocus = true/false
tea.SetWindowTitle("...")view.WindowTitle = "..."

Removed Options (No Replacement Needed)

v1 OptionWhat Happened
tea.WithInputTTY()v2 always opens the TTY for input automatically
tea.WithANSICompressor()The new renderer handles optimization automatically

Removed Program Methods

v1 Methodv2 Replacement
p.Start()p.Run()
p.StartReturningModel()p.Run()
p.EnterAltScreen()view.AltScreen = true in View()
p.ExitAltScreen()view.AltScreen = false in View()
p.EnableMouseCellMotion()view.MouseMode in View()
p.DisableMouseCellMotion()view.MouseMode = tea.MouseModeNone in View()
p.EnableMouseAllMotion()view.MouseMode in View()
p.DisableMouseAllMotion()view.MouseMode = tea.MouseModeNone in View()
p.SetWindowTitle(...)view.WindowTitle in View()

Other Renames

v1v2
tea.Sequentially(...)tea.Sequence(...)
tea.WindowSize()tea.RequestWindowSize (now returns Msg, not Cmd)

New Program Options

OptionDescription
tea.WithColorProfile(p)Force a specific color profile
tea.WithWindowSize(w, h)Set initial window size (great for testing)

Feedback

Have thoughts on the v2 upgrade? We’d love to hear about it. Let us know on:

Build docs developers (and LLMs) love