Views are how you render your application’s user interface. They declare what should be displayed, and Bubble Tea handles how to render it efficiently.
The View Type
From tea.go:81-189 :
// View represents a terminal view that can be composed of multiple layers.
// It can also contain a cursor that will be rendered on top of the layers.
type View struct {
// Content is the screen content of the view. It holds styled strings that
// will be rendered to the terminal when the view is rendered.
Content string
// OnMouse is an optional mouse message handler
OnMouse func ( msg MouseMsg ) Cmd
// Cursor represents the cursor position, style, and visibility
Cursor * Cursor
// BackgroundColor sets the terminal background color
BackgroundColor color . Color
// ForegroundColor sets the terminal foreground color
ForegroundColor color . Color
// WindowTitle sets the terminal window title
WindowTitle string
// ProgressBar shows a progress bar in the terminal
ProgressBar * ProgressBar
// AltScreen puts the program in alternate screen buffer (full window mode)
AltScreen bool
// ReportFocus enables reporting when the terminal gains and loses focus
ReportFocus bool
// DisableBracketedPasteMode disables bracketed paste mode
DisableBracketedPasteMode bool
// MouseMode sets the mouse mode
MouseMode MouseMode
// KeyboardEnhancements describes keyboard enhancement features to request
KeyboardEnhancements KeyboardEnhancements
}
Creating Views
NewView
The simplest way to create a view:
From tea.go:67-79 :
// NewView is a helper function to create a new [View] with the given styled
// string.
func NewView ( s string ) View
Example:
func ( m model ) View () tea . View {
return tea . NewView ( "Hello, World!" )
}
Manual Construction
For more control, create the View directly:
func ( m model ) View () tea . View {
var v tea . View
v . Content = m . render ()
v . WindowTitle = "My App"
v . MouseMode = tea . MouseModeAllMotion
return v
}
SetContent Method
From tea.go:249-261 :
// SetContent is a helper method to set the content of a [View]
func ( v * View ) SetContent ( s string )
Example:
func ( m model ) View () tea . View {
var v tea . View
v . SetContent ( "Hello!" )
return v
}
Content Rendering
Basic Content
Content is a string with ANSI escape codes for styling:
func ( m model ) View () tea . View {
s := "Counter: " + strconv . Itoa ( m . count ) + " \n "
s += "Press 'q' to quit \n "
return tea . NewView ( s )
}
Using Lip Gloss for Styling
Lip Gloss is the recommended way to style terminal output:
import " github.com/charmbracelet/lipgloss "
var (
titleStyle = lipgloss . NewStyle ().
Bold ( true ).
Foreground ( lipgloss . Color ( "#FAFAFA" )).
Background ( lipgloss . Color ( "#7D56F4" )).
Padding ( 0 , 1 )
textStyle = lipgloss . NewStyle ().
Foreground ( lipgloss . Color ( "#999999" ))
)
func ( m model ) View () tea . View {
title := titleStyle . Render ( "My Application" )
text := textStyle . Render ( "Welcome to Bubble Tea!" )
content := lipgloss . JoinVertical ( lipgloss . Left ,
title ,
"" ,
text ,
)
return tea . NewView ( content )
}
Multi-line Content
Use string concatenation or strings.Builder:
String Concatenation
strings.Builder
fmt.Sprintf
func ( m model ) View () tea . View {
s := ""
s += "Line 1 \n "
s += "Line 2 \n "
s += "Line 3 \n "
return tea . NewView ( s )
}
func ( m model ) View () tea . View {
var b strings . Builder
b . WriteString ( "Line 1 \n " )
b . WriteString ( "Line 2 \n " )
b . WriteString ( "Line 3 \n " )
return tea . NewView ( b . String ())
}
func ( m model ) View () tea . View {
s := fmt . Sprintf ( `
Line 1
Line 2
Line 3
` )
return tea . NewView ( s )
}
View Features
Window Title
Set the terminal window title:
func ( m model ) View () tea . View {
v := tea . NewView ( m . render ())
v . WindowTitle = fmt . Sprintf ( "MyApp - %s " , m . currentFile )
return v
}
See tutorials/basics/main.go:79 for an example.
Alternate Screen
Switch to full-screen mode:
func ( m model ) View () tea . View {
v := tea . NewView ( m . render ())
v . AltScreen = true // Full window mode
return v
}
Alternate Screen (AltScreen) is like what you see in vim or less - when you exit, the terminal returns to its previous state.
Cursor
From tea.go:336-361 :
// Cursor represents a cursor on the terminal screen.
type Cursor struct {
Position // X, Y coordinates
Color color . Color
Shape CursorShape
Blink bool
}
func NewCursor ( x , y int ) * Cursor
Example:
func ( m model ) View () tea . View {
v := tea . NewView ( m . render ())
// Show cursor at input position
v . Cursor = tea . NewCursor ( m . cursorX , m . cursorY )
v . Cursor . Shape = tea . CursorBar
v . Cursor . Blink = true
return v
}
Cursor Shapes:
CursorBlock - Block cursor (█)
CursorUnderline - Underline cursor (_)
CursorBar - Bar cursor (|)
Mouse Support
From tea.go:263-286 :
type MouseMode int
const (
MouseModeNone // No mouse events
MouseModeCellMotion // Click, release, wheel, and drag events
MouseModeAllMotion // All mouse events including movement
)
Enable mouse support:
func ( m model ) View () tea . View {
v := tea . NewView ( m . render ())
v . MouseMode = tea . MouseModeAllMotion
return v
}
Handle mouse events:
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . MouseClickMsg :
mouse := msg . Mouse ()
m . handleClick ( mouse . X , mouse . Y )
}
return m , nil
}
OnMouse Handler
Handle mouse events based on view content:
From tea.go:97-125 :
func ( m model ) View () tea . View {
content := "Click [here] to continue"
v := tea . NewView ( content )
v . OnMouse = func ( msg tea . MouseMsg ) tea . Cmd {
mouse := msg . Mouse ()
// Check if click is on "here"
start := strings . Index ( content , "[here]" )
end := start + 6
if mouse . Y == 0 && mouse . X >= start && mouse . X < end {
return func () tea . Msg {
return clickedHereMsg {}
}
}
return nil
}
return v
}
Progress Bar
From tea.go:311-334 :
type ProgressBar struct {
State ProgressBarState
Value int // 0-100
}
const (
ProgressBarNone
ProgressBarDefault
ProgressBarError
ProgressBarIndeterminate
ProgressBarWarning
)
func NewProgressBar ( state ProgressBarState , value int ) * ProgressBar
Example:
func ( m model ) View () tea . View {
v := tea . NewView ( m . render ())
// Show download progress
v . ProgressBar = tea . NewProgressBar (
tea . ProgressBarDefault ,
m . downloadPercent ,
)
return v
}
Progress bar support depends on the terminal. Windows Terminal and some other modern terminals support this feature.
Terminal Colors
Set default terminal colors:
import " image/color "
func ( m model ) View () tea . View {
v := tea . NewView ( m . render ())
v . BackgroundColor = color . RGBA { 0x 1a , 0x 1b , 0x 26 , 0x ff }
v . ForegroundColor = color . RGBA { 0x c0 , 0x ca , 0x f5 , 0x ff }
return v
}
Keyboard Enhancements
Request advanced keyboard features:
From tea.go:195-247 :
type KeyboardEnhancements struct {
// ReportEventTypes requests key repeat and release events
ReportEventTypes bool
}
Example:
func ( m model ) View () tea . View {
v := tea . NewView ( m . render ())
// Request key release events
v . KeyboardEnhancements . ReportEventTypes = true
return v
}
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . KeyboardEnhancementsMsg :
if msg . ReportEventTypes {
// We can now receive KeyReleaseMsg!
}
case tea . KeyReleaseMsg :
// Handle key release
}
return m , nil
}
View Patterns
Responsive Layout
Adjust to terminal size:
func ( m model ) View () tea . View {
if m . width < 80 {
// Narrow layout
return tea . NewView ( m . renderNarrow ())
}
// Wide layout
return tea . NewView ( m . renderWide ())
}
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . WindowSizeMsg :
m . width = msg . Width
m . height = msg . Height
}
return m , nil
}
Component Composition
type model struct {
header headerComponent
body bodyComponent
footer footerComponent
}
func ( m model ) View () tea . View {
var b strings . Builder
// Compose views from components
b . WriteString ( m . header . View ())
b . WriteString ( " \n " )
b . WriteString ( m . body . View ())
b . WriteString ( " \n " )
b . WriteString ( m . footer . View ())
return tea . NewView ( b . String ())
}
Conditional Rendering
func ( m model ) View () tea . View {
switch m . state {
case stateLoading :
return tea . NewView ( m . renderLoading ())
case stateError :
return tea . NewView ( m . renderError ())
case stateSuccess :
return tea . NewView ( m . renderContent ())
default :
return tea . NewView ( "" )
}
}
With Lip Gloss Layout
import " github.com/charmbracelet/lipgloss "
func ( m model ) View () tea . View {
// Create styled boxes
leftBox := lipgloss . NewStyle ().
Border ( lipgloss . NormalBorder ()).
Width ( m . width / 2 ).
Render ( m . leftContent )
rightBox := lipgloss . NewStyle ().
Border ( lipgloss . NormalBorder ()).
Width ( m . width / 2 ).
Render ( m . rightContent )
// Combine horizontally
content := lipgloss . JoinHorizontal ( lipgloss . Top , leftBox , rightBox )
return tea . NewView ( content )
}
Best Practices
Never modify the model in View. Only read from it. // ❌ Bad
func ( m model ) View () tea . View {
m . viewCount ++ // Don't mutate!
return tea . NewView ( m . render ())
}
// ✅ Good
func ( m model ) View () tea . View {
return tea . NewView ( m . render ())
}
Don't Worry About Performance
Break complex views into smaller functions. func ( m model ) View () tea . View {
return tea . NewView ( lipgloss . JoinVertical ( lipgloss . Left ,
m . renderHeader (),
m . renderBody (),
m . renderFooter (),
))
}
func ( m model ) renderHeader () string { /* ... */ }
func ( m model ) renderBody () string { /* ... */ }
func ( m model ) renderFooter () string { /* ... */ }
If rendering is expensive, cache the result in your model. type model struct {
data [] Item
dataHash uint64
cachedView string
}
func ( m model ) View () tea . View {
hash := hashData ( m . data )
if hash != m . dataHash {
m . dataHash = hash
m . cachedView = m . expensiveRender ()
}
return tea . NewView ( m . cachedView )
}
Complete Example
Here’s a full example showing various view features:
package main
import (
" fmt "
" strings "
tea " charm.land/bubbletea/v2 "
" github.com/charmbracelet/lipgloss "
)
type model struct {
width int
height int
inputText string
cursorPos int
}
var (
titleStyle = lipgloss . NewStyle ().
Bold ( true ).
Background ( lipgloss . Color ( "#7D56F4" )).
Padding ( 0 , 1 )
inputStyle = lipgloss . NewStyle ().
Border ( lipgloss . RoundedBorder ()).
BorderForeground ( lipgloss . Color ( "#7D56F4" ))
)
func ( m model ) Init () tea . Cmd {
return nil
}
func ( m model ) Update ( msg tea . Msg ) ( tea . Model , tea . Cmd ) {
switch msg := msg .( type ) {
case tea . WindowSizeMsg :
m . width = msg . Width
m . height = msg . Height
case tea . KeyPressMsg :
switch msg . String () {
case "ctrl+c" :
return m , tea . Quit
case "backspace" :
if len ( m . inputText ) > 0 {
m . inputText = m . inputText [: len ( m . inputText ) - 1 ]
m . cursorPos --
}
default :
if len ( msg . String ()) == 1 {
m . inputText += msg . String ()
m . cursorPos ++
}
}
}
return m , nil
}
func ( m model ) View () tea . View {
var v tea . View
// Build content
title := titleStyle . Render ( "Text Input Demo" )
input := inputStyle . Render ( m . inputText + "_" )
help := "Type to input text. Press ctrl+c to quit."
content := lipgloss . JoinVertical ( lipgloss . Left ,
title ,
"" ,
input ,
"" ,
help ,
)
// Center the content
if m . width > 0 && m . height > 0 {
content = lipgloss . Place ( m . width , m . height ,
lipgloss . Center , lipgloss . Center ,
content ,
)
}
v . SetContent ( content )
// Set view options
v . WindowTitle = "Bubble Tea Demo"
v . MouseMode = tea . MouseModeCellMotion
// Show cursor at input position
if len ( m . inputText ) > 0 {
v . Cursor = tea . NewCursor ( m . cursorPos , 2 )
}
return v
}
func main () {
p := tea . NewProgram ( model {})
if _ , err := p . Run (); err != nil {
fmt . Printf ( "Error: %v " , err )
}
}
Next Steps
Lip Gloss Learn to style your terminal UI with Lip Gloss
Bubbles Use pre-built UI components
Examples See real-world view implementations