Skip to main content
In this tutorial, you’ll build an interactive shopping list application that demonstrates the core concepts of Bubble Tea.

What you’ll build

A terminal UI where users can:
  • Navigate through a list of items with arrow keys
  • Select and deselect items with the spacebar or Enter
  • Quit the program with q or Ctrl+C

Create your project

1

Set up your project directory

mkdir shopping-list
cd shopping-list
go mod init shopping-list
go get charm.land/bubbletea/v2
2

Create main.go

Create a new file called main.go and follow along with the steps below.

Build the application

Import dependencies

Start by importing the necessary packages:
main.go
package main

import (
	"fmt"
	"os"

	tea "charm.land/bubbletea/v2"
)

Define the model

The model represents your application’s state. For this shopping list, you need to track the available choices, cursor position, and which items are selected:
main.go
type model struct {
	cursor   int                // which item our cursor is pointing at
	choices  []string           // items on the list
	selected map[int]struct{}   // which items are selected
}

Initialize the model

Create a function that returns the initial state of your application:
main.go
func initialModel() model {
	return model{
		// Our to-do list is a grocery list
		choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},

		// A map which indicates which choices are selected. We're using
		// the map like a mathematical set. The keys refer to the indexes
		// of the `choices` slice, above.
		selected: make(map[int]struct{}),
	}
}

Implement Init

The Init method returns an initial command. For this simple app, we don’t need any initial I/O, so return nil:
main.go
func (m model) Init() tea.Cmd {
	return nil
}
Init can return commands for initial I/O operations like fetching data from an API or reading a file. You’ll learn more about commands in the concepts section.

Implement Update

The Update method handles all events. It receives a message, updates the model, and optionally returns a command:
main.go
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 "up", "k":
			if m.cursor > 0 {
				m.cursor--
			}
		case "down", "j":
			if m.cursor < len(m.choices)-1 {
				m.cursor++
			}
		case "enter", "space":
			_, ok := m.selected[m.cursor]
			if ok {
				delete(m.selected, m.cursor)
			} else {
				m.selected[m.cursor] = struct{}{}
			}
		}
	}

	return m, nil
}
This Update method:
  • Uses a type switch to determine the message type
  • Handles KeyPressMsg for keyboard input
  • Moves the cursor up/down with arrow keys or vim bindings
  • Toggles selection with Enter or spacebar
  • Returns tea.Quit command to exit the program

Implement View

The View method renders your UI based on the current model state:
main.go
func (m model) View() tea.View {
	s := "What should we buy at the market?\n\n"

	for i, choice := range m.choices {
		cursor := " "
		if m.cursor == i {
			cursor = ">"
		}

		checked := " "
		if _, ok := m.selected[i]; ok {
			checked = "x"
		}

		s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
	}

	s += "\nPress q to quit.\n"

	return tea.NewView(s)
}
The View method:
  • Builds a string representation of your UI
  • Shows a cursor (>) next to the current item
  • Shows a checkmark (x) for selected items
  • Returns a tea.View containing the UI content
You don’t need to worry about redrawing logic. Bubble Tea automatically handles rendering when the model changes.

Create the main function

Finally, create the main function to run your program:
main.go
func main() {
	p := tea.NewProgram(initialModel())
	if _, err := p.Run(); err != nil {
		fmt.Printf("Alas, there's been an error: %v", err)
		os.Exit(1)
	}
}

Run your program

Execute your application:
go run main.go
You should see:
  • A list of grocery items
  • A cursor (>) pointing at the first item
  • The ability to navigate with arrow keys
  • The ability to select items with spacebar or Enter

Understanding the flow

1

Initialization

main() calls tea.NewProgram() with your initial model, then Run() starts the program.
2

Init is called

The Init() method runs and can return an initial command (we return nil).
3

View renders

The View() method is called to render the initial UI.
4

Update loop

When you press a key:
  1. Bubble Tea sends a KeyPressMsg to Update()
  2. Update() modifies the model based on the key pressed
  3. View() is called again to re-render with the updated model
5

Exit

When you press q or Ctrl+C, Update() returns tea.Quit, and the program exits.

Next steps

Core concepts

Dive deeper into models, messages, and commands

Commands guide

Learn how to perform I/O operations

Examples

Explore more complex Bubble Tea applications

Bubbles components

Use pre-built UI components like text inputs and spinners

Build docs developers (and LLMs) love