Skip to main content
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

Build docs developers (and LLMs) love