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

Embed child models in your parent model and delegate messages:
type model struct {
state int
spinner spinner.Model
timer timer.Model
}
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()
}
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.
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.
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.