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.
Restricts the maximum input length.
Sets which input field receives keyboard input.
func ( m model ) Init () tea . Cmd {
return textinput . Blink
}
Returns the blink command to animate the cursor.
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 ... )
Hide sensitive input with echo mode:
t . EchoMode = textinput . EchoPassword
t . EchoCharacter = ' • '
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