Tutorials

Composable Views

Combine multiple Bubble Tea models into one application.

Real applications need multiple components working together. Here's how to compose Bubble Tea models like a pro.

The Pattern

Embed child models in your parent model and delegate messages:

type model struct {
    state   int
    spinner spinner.Model
    timer   timer.Model
}

Complete Example

This app switches between a spinner and a timer:

package main

import (
    "github.com/charmbracelet/bubbles/v2/spinner"
    "github.com/charmbracelet/bubbles/v2/timer"
    tea "github.com/charmbracelet/bubbletea/v2"
    "github.com/charmbracelet/lipgloss/v2"
    "time"
)

type model struct {
    state   int  // 0 = spinner, 1 = timer
    spinner spinner.Model
    timer   timer.Model
}

func newModel() model {
    s := spinner.New()
    s.Spinner = spinner.Dot
    s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))

    t := timer.NewWithInterval(5*time.Second, time.Millisecond*100)

    return model{spinner: s, timer: t}
}

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

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch msg.String() {
        case "ctrl+c", "q":
            return m, tea.Quit
        case "tab":
            // Switch between views
            m.state = (m.state + 1) % 2
            if m.state == 1 {
                return m, m.timer.Init()
            }
            return m, m.spinner.Tick
        }
    case timer.TimeoutMsg:
        m.state = 0
        return m, m.spinner.Tick
    }

    var cmd tea.Cmd
    switch m.state {
    case 0:
        m.spinner, cmd = m.spinner.Update(msg)
    case 1:
        m.timer, cmd = m.timer.Update(msg)
    }
    return m, cmd
}

func (m model) View() string {
    switch m.state {
    case 1:
        return "Timer: " + m.timer.View() + "\n\nPress tab to switch, q to quit"
    default:
        return m.spinner.View() + " Loading...\n\nPress tab to switch, q to quit"
    }
}

func main() {
    tea.NewProgram(newModel()).Run()
}

Key Concepts

Delegate to the Active Component

switch m.state {
case 0:
    m.spinner, cmd = m.spinner.Update(msg)
case 1:
    m.timer, cmd = m.timer.Update(msg)
}

Only update the component that's currently active.

Switch Init Commands

case "tab":
    m.state = (m.state + 1) % 2
    if m.state == 1 {
        return m, m.timer.Init()  // Start the timer
    }
    return m, m.spinner.Tick  // Start the spinner

When switching views, return the appropriate init command.

Handle Child Completion

case timer.TimeoutMsg:
    m.state = 0  // Go back to spinner when timer ends
    return m, m.spinner.Tick

Watch for completion messages from child components.

This pattern scales! You can have a dozen embedded models - just keep track of which one is active.