Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/charmbracelet/bubbletea/llms.txt

Use this file to discover all available pages before exploring further.

Bubble Tea provides powerful components for building forms with single or multiple input fields, validation, and focus management.

Basic Text Input

The simplest form is a single text input field. Here’s an example from examples/textinput/main.go:
package main

import (
	"log"

	"charm.land/bubbles/v2/textinput"
	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
)

type model struct {
	textInput textinput.Model
	err       error
	quitting  bool
}

func initialModel() model {
	ti := textinput.New()
	ti.Placeholder = "Pikachu"
	ti.SetVirtualCursor(false)
	ti.Focus()
	ti.CharLimit = 156
	ti.SetWidth(20)

	return model{textInput: ti}
}

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

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmd tea.Cmd

	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		switch msg.String() {
		case "enter", "ctrl+c", "esc":
			m.quitting = true
			return m, tea.Quit
		}
	}

	m.textInput, cmd = m.textInput.Update(msg)
	return m, cmd
}

func (m model) View() tea.View {
	var c *tea.Cursor
	if !m.textInput.VirtualCursor() {
		c = m.textInput.Cursor()
		c.Y += lipgloss.Height(m.headerView())
	}

	str := lipgloss.JoinVertical(lipgloss.Top, m.headerView(), m.textInput.View(), m.footerView())
	if m.quitting {
		str += "\n"
	}

	v := tea.NewView(str)
	v.Cursor = c
	return v
}

func (m model) headerView() string { return "What's your favorite Pokémon?\n" }
func (m model) footerView() string { return "\n(esc to quit)" }

Key Features

ti.Placeholder = "Pikachu"
Displays hint text when the field is empty.
ti.CharLimit = 156
Restricts the maximum input length.
ti.Focus()
Sets which input field receives keyboard input.
func (m model) Init() tea.Cmd {
    return textinput.Blink
}
Returns the blink command to animate the cursor.

Multiple Inputs with Focus

The textinputs example shows how to manage multiple input fields from examples/textinputs/main.go:
type model struct {
	focusIndex int
	inputs     []textinput.Model
	cursorMode cursor.Mode
	quitting   bool
}

func initialModel() model {
	m := model{
		inputs: make([]textinput.Model, 3),
	}

	var t textinput.Model
	for i := range m.inputs {
		t = textinput.New()
		t.CharLimit = 32

		s := t.Styles()
		s.Cursor.Color = lipgloss.Color("205")
		s.Focused.Prompt = focusedStyle
		s.Focused.Text = focusedStyle
		s.Blurred.Prompt = blurredStyle
		s.Focused.Text = focusedStyle
		t.SetStyles(s)

		switch i {
		case 0:
			t.Placeholder = "Nickname"
			t.Focus()
		case 1:
			t.Placeholder = "Email"
			t.CharLimit = 64
		case 2:
			t.Placeholder = "Password"
			t.EchoMode = textinput.EchoPassword
			t.EchoCharacter = ''
		}

		m.inputs[i] = t
	}

	return m
}

Focus Switching

Handle tab navigation between fields:
case "tab", "shift+tab", "enter", "up", "down":
	s := msg.String()

	if s == "enter" && m.focusIndex == len(m.inputs) {
		return m, tea.Quit
	}

	// Cycle indexes
	if s == "up" || s == "shift+tab" {
		m.focusIndex--
	} else {
		m.focusIndex++
	}

	if m.focusIndex > len(m.inputs) {
		m.focusIndex = 0
	} else if m.focusIndex < 0 {
		m.focusIndex = len(m.inputs)
	}

	cmds := make([]tea.Cmd, len(m.inputs))
	for i := 0; i <= len(m.inputs)-1; i++ {
		if i == m.focusIndex {
			cmds[i] = m.inputs[i].Focus()
			continue
		}
		m.inputs[i].Blur()
	}

	return m, tea.Batch(cmds...)

Password Input

Hide sensitive input with echo mode:
t.EchoMode = textinput.EchoPassword
t.EchoCharacter = ''

Form Validation

The ISBN form example (examples/isbn-form/main.go) demonstrates input validation:
func isbn13Validator(s string) error {
	// Remove dashes
	s = strings.ReplaceAll(s, "-", "")
	if len(s) != 13 {
		return fmt.Errorf("ISBN is of wrong length")
	}

	for _, c := range s {
		if !unicode.IsDigit(c) {
			return fmt.Errorf("ISBN contains invalid characters")
		}
	}

	gs1Prefix := s[:3]
	switch gs1Prefix {
	case "978", "979":
		break
	default:
			return fmt.Errorf("ISBN has invalid GS1 prefix")
	}

	// Checksum validation
	sum := 0
	for i, c := range s {
		n := int(c - '0')
		if i%2 != 0 {
			n *= 3
		}
		sum += n
	}

	if sum%10 != 0 {
			return fmt.Errorf("ISBN has invalid check digit")
	}

	return nil
}

func initialModel() model {
	isbnInput := textinput.New()
	isbnInput.Focus()
	isbnInput.Placeholder = "978-X-XXX-XXXXX-X"
	isbnInput.CharLimit = 17
	isbnInput.Validate = isbn13Validator

	return model{isbnInput: isbnInput}
}

Displaying Validation Errors

var isbnErrorText string
if m.isbnInput.Value() != "" {
	if m.isbnInput.Err != nil {
		isbnErrorText = errStyle.Render(m.isbnInput.Err.Error())
	} else {
		isbnErrorText = validStyle.Render("Valid ISBN")
	}
}

Conditional Submission

func (m model) canFindBook() bool {
	correctIsbnGiven := m.isbnInput.Err == nil && len(m.isbnInput.Value()) != 0
	correctTitleGiven := m.titleInput.Err == nil && len(m.titleInput.Value()) != 0

	return correctIsbnGiven && correctTitleGiven
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		switch msg.String() {
		case "enter":
			if m.canFindBook() {
				return m, tea.Quit
			}
		}
	}
	// ...
}

Best Practices

Use Validators

Attach validation functions to provide immediate feedback

Focus Management

Always track which input has focus and handle tab navigation

Visual Feedback

Use different styles for focused/blurred and valid/invalid states

Batch Commands

Use tea.Batch() when updating multiple inputs

Running the Examples

# Single text input
cd examples/textinput
go run .

# Multiple inputs
cd examples/textinputs
go run .

# Form with validation
cd examples/isbn-form
go run .

Source Code