Skip to main content
Messages (Msgs) are the events that drive your Bubble Tea application. Every interaction, timer tick, HTTP response, and system event becomes a message that flows through your Update method.

What Are Messages?

From tea.go:47-49:
// Msg contain data from the result of a IO operation. Msgs trigger the update
// function and, henceforth, the UI.
type Msg = uv.Event
A Msg can be any type. This flexibility lets you define custom messages that perfectly match your application’s needs.

The Message Flow

  1. Something happens (key press, timer, etc.)
  2. Bubble Tea wraps it in a Msg
  3. Your Update method receives it
  4. You return an updated model
  5. Bubble Tea calls View and renders

Built-in Message Types

Bubble Tea provides several built-in message types for common events.

Keyboard Messages

KeyPressMsg

The most common message - sent when a key is pressed. From key.go:190-221:
// KeyPressMsg represents a key press message.
type KeyPressMsg Key

func (k KeyPressMsg) String() string
func (k KeyPressMsg) Keystroke() string
func (k KeyPressMsg) Key() Key
Usage:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        // Method 1: String matching (simpler)
        switch msg.String() {
        case "ctrl+c", "q":
            return m, tea.Quit
        case "up", "k":
            m.cursor--
        case "enter":
            return m, m.submit()
        }
        
        // Method 2: Key code matching (more precise)
        key := msg.Key()
        switch key.Code {
        case tea.KeyEnter:
            return m, m.submit()
        case tea.KeyUp:
            m.cursor--
        }
    }
    return m, nil
}

Key Structure

From key.go:302-343:
type Key struct {
    // Text contains the actual characters received
    Text string
    
    // Mod represents modifier keys (Ctrl, Alt, Shift, etc.)
    Mod KeyMod
    
    // Code represents the key pressed
    Code rune
    
    // ShiftedCode is the shifted key (e.g., 'A' when shift+a)
    ShiftedCode rune
    
    // BaseCode is the key on a standard PC-101 layout
    BaseCode rune
    
    // IsRepeat indicates if key is being held down
    IsRepeat bool
}
Common Key Codes:
tea.KeyEnter     // Enter/Return key
tea.KeySpace     // Space bar
tea.KeyBackspace // Backspace
tea.KeyTab       // Tab
tea.KeyEsc       // Escape

// Arrow keys
tea.KeyUp
tea.KeyDown
tea.KeyLeft
tea.KeyRight

// Function keys
tea.KeyF1 through tea.KeyF63

// Special keys
tea.KeyHome
tea.KeyEnd
tea.KeyPgUp
tea.KeyPgDown
tea.KeyDelete
tea.KeyInsert
See key.go:16-188 for the complete list.

KeyReleaseMsg

Sent when a key is released (requires keyboard enhancements).
case tea.KeyReleaseMsg:
    fmt.Println("Key released:", msg.String())

Mouse Messages

From mouse.go:44-51:
// MouseMsg represents a mouse message. This is a generic mouse message that
// can represent any kind of mouse event.
type MouseMsg interface {
    fmt.Stringer
    Mouse() Mouse
}

Mouse Structure

type Mouse struct {
    X, Y   int          // Position (0-based from top-left)
    Button MouseButton  // Which button
    Mod    KeyMod       // Modifier keys held
}

Mouse Button Constants

From mouse.go:29-42:
tea.MouseNone        // No button
tea.MouseLeft        // Left button
tea.MouseMiddle      // Middle button
tea.MouseRight       // Right button
tea.MouseWheelUp     // Scroll up
tea.MouseWheelDown   // Scroll down
tea.MouseWheelLeft   // Scroll left
tea.MouseWheelRight  // Scroll right
tea.MouseBackward    // Browser back button
tea.MouseForward     // Browser forward button

Mouse Event Types

case tea.MouseClickMsg:
    m := msg.Mouse()
    if m.Button == tea.MouseLeft {
        fmt.Printf("Clicked at (%d, %d)\n", m.X, m.Y)
    }
Mouse events require enabling mouse mode in your view:
func (m model) View() tea.View {
    v := tea.NewView(m.render())
    v.MouseMode = tea.MouseModeAllMotion  // Enable all mouse events
    return v
}

Window Messages

WindowSizeMsg

Sent when the terminal is resized or when the program starts.
case tea.WindowSizeMsg:
    m.width = msg.Width
    m.height = msg.Height
    // Resize child components
    m.viewport.Width = msg.Width
    m.viewport.Height = msg.Height - 2
You’ll receive a WindowSizeMsg immediately when your program starts, giving you the initial terminal dimensions.

System Messages

QuitMsg

Signals the program should quit. From tea.go:534-541:
// QuitMsg signals that the program should quit.
type QuitMsg struct{}

// Quit is a special command that tells the Bubble Tea program to exit.
func Quit() Msg {
    return QuitMsg{}
}
Usage:
case tea.KeyPressMsg:
    if msg.String() == "q" {
        return m, tea.Quit  // Returns a command that sends QuitMsg
    }

case tea.QuitMsg:
    // Handle cleanup before quitting
    return m, nil

InterruptMsg

Sent when Ctrl+C is pressed (when not caught as a key press). From tea.go:560-572:
// InterruptMsg signals the program should interrupt.
type InterruptMsg struct{}

func Interrupt() Msg {
    return InterruptMsg{}
}

SuspendMsg / ResumeMsg

Sent when the program is suspended (Ctrl+Z) and resumed. From tea.go:543-558:
// SuspendMsg signals the program should suspend.
type SuspendMsg struct{}

func Suspend() Msg {
    return SuspendMsg{}
}

// ResumeMsg can be listened to do something once a program is resumed back
// from a suspend state.
type ResumeMsg struct{}
Example:
case tea.SuspendMsg:
    // Save state before suspending
    m.saveState()
    return m, tea.Suspend

case tea.ResumeMsg:
    // Refresh after resuming
    return m, m.fetchLatestData()

Color and Terminal Info

ColorProfileMsg

Sent when the terminal’s color capabilities are detected.
case tea.ColorProfileMsg:
    m.profile = msg.Profile
    // Adjust colors based on capabilities

Custom Messages

The real power of messages is defining your own types.

Defining Custom Messages

// Simple message with no data
type tickMsg time.Time

// Message with data
type userLoadedMsg struct {
    user User
}

// Error message
type errMsg struct {
    err error
}
By convention, custom message types end with Msg, but this isn’t required.

Using Custom Messages

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    
    case tickMsg:
        m.lastTick = time.Time(msg)
        return m, tick()  // Schedule next tick
    
    case userLoadedMsg:
        m.user = msg.user
        m.loading = false
        return m, nil
    
    case errMsg:
        m.err = msg.err
        m.loading = false
        return m, nil
    }
    return m, nil
}

Creating Custom Messages from Commands

Commands return messages:
func fetchUser(id int) tea.Cmd {
    return func() tea.Msg {
        user, err := api.GetUser(id)
        if err != nil {
            return errMsg{err}
        }
        return userLoadedMsg{user}
    }
}

// Use in Update
case tea.KeyPressMsg:
    if msg.String() == "enter" {
        m.loading = true
        return m, fetchUser(m.selectedID)
    }
See examples/simple/main.go:73-78 for a real example.

Message Patterns

The Tick Pattern

type tickMsg time.Time

func tick() tea.Cmd {
    return tea.Tick(time.Second, func(t time.Time) tea.Msg {
        return tickMsg(t)
    })
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg.(type) {
    case tickMsg:
        m.counter++
        return m, tick()  // Schedule next tick
    }
    return m, nil
}

The Request/Response Pattern

// Request
type loadDataMsg struct{}

// Responses
type dataLoadedMsg struct {
    data []Item
}
type dataErrorMsg struct {
    err error
}

func loadData() tea.Cmd {
    return func() tea.Msg {
        data, err := fetchData()
        if err != nil {
            return dataErrorMsg{err}
        }
        return dataLoadedMsg{data}
    }
}

The State Machine Pattern

type state int

const (
    stateLoading state = iota
    stateReady
    stateError
)

type stateChangeMsg struct {
    newState state
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case stateChangeMsg:
        m.state = msg.newState
        switch msg.newState {
        case stateLoading:
            return m, loadData()
        case stateReady:
            return m, nil
        case stateError:
            return m, nil
        }
    }
    return m, nil
}

Best Practices

Always use type switches to handle messages:
// Good
switch msg := msg.(type) {
case tea.KeyPressMsg:
    // msg is now KeyPressMsg
}

// Bad - loses type information
switch msg.(type) {
case tea.KeyPressMsg:
    // msg is still tea.Msg
}
Always have a default case that returns the model unchanged:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case myMsg:
        // Handle
    default:
        // Don't drop messages you don't recognize
    }
    return m, nil
}
Messages should be plain data - no methods, no behavior:
// Good
type userLoadedMsg struct {
    user User
}

// Bad - too much behavior
type userLoadedMsg struct {
    user User
}
func (m userLoadedMsg) Process() { /* ... */ }
Add comments explaining when and why messages are sent:
// tickMsg is sent every second to update the countdown timer.
type tickMsg time.Time

// dataLoadedMsg is sent when the API request completes successfully.
type dataLoadedMsg struct {
    items []Item
}

Next Steps

Commands

Learn how to create commands that generate messages

Model

Understand how messages update your model

Views

See how model updates trigger rendering

Build docs developers (and LLMs) love