Skip to main content

Overview

Bubble Tea supports terminal styling through ANSI codes and integrates seamlessly with Lip Gloss, Charm’s styling library for terminal applications. The framework automatically handles color profile detection and downsampling.

Lip Gloss Integration

The recommended way to style Bubble Tea applications is with Lip Gloss:
examples/spinner/main.go:12-28
import (
    "charm.land/bubbles/v2/spinner"
    tea "charm.land/bubbletea/v2"
    "charm.land/lipgloss/v2"
)

func initialModel() model {
    s := spinner.New()
    s.Spinner = spinner.Dot
    s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
    return model{spinner: s}
}

Basic Styling

import "charm.land/lipgloss/v2"

var (
    titleStyle = lipgloss.NewStyle().
        Bold(true).
        Foreground(lipgloss.Color("#FAFAFA")).
        Background(lipgloss.Color("#7D56F4")).
        Padding(0, 1)

    errorStyle = lipgloss.NewStyle().
        Foreground(lipgloss.Color("#FF0000")).
        Bold(true)

    subtleStyle = lipgloss.NewStyle().
        Foreground(lipgloss.Color("#888888"))
)

func (m model) View() tea.View {
    title := titleStyle.Render("My Application")
    subtitle := subtleStyle.Render("Press q to quit")
    
    content := title + "\n" + subtitle
    return tea.NewView(content)
}

Color Profiles

Bubble Tea automatically detects the terminal’s color capabilities:
  • TrueColor (24-bit) - 16 million colors
  • ANSI256 - 256 colors
  • ANSI - 16 colors
  • Ascii - No colors

Automatic Detection

Color profiles are detected from environment variables and terminal capabilities:
// Bubble Tea automatically detects color support
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
    log.Fatal(err)
}

Manual Color Profile

Override the detected color profile:
examples/colorprofile/main.go:46-52
import "github.com/charmbracelet/colorprofile"

func main() {
    p := tea.NewProgram(model{}, tea.WithColorProfile(colorprofile.TrueColor))
    if _, err := p.Run(); err != nil {
        log.Fatal(err)
    }
}

Color Profile Messages

React to color profile changes:
examples/colorprofile/main.go:28-35
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.ColorProfileMsg:
        return m, tea.Println("Color profile manually set to ", msg)
    }
    return m, nil
}

ANSI Styling

For direct ANSI control, use the ansi package:
examples/colorprofile/main.go:39-44
import "github.com/charmbracelet/x/ansi"

func (m model) View() tea.View {
    styled := ansi.Style{}.ForegroundColor(myFancyColor).Styled("Howdy!")
    return tea.NewView(styled)
}

ANSI Color Codes

import "github.com/charmbracelet/x/ansi"

// Basic colors
red := ansi.Red
green := ansi.Green
blue := ansi.Blue

// 256 colors
color := ansi.ExtendedColor(196)  // Bright red

// True color
import "github.com/lucasb-eyer/go-colorful"
color, _ := colorful.Hex("#6b50ff")

Dynamic Background Colors

Query the terminal’s background color to adapt your styles:
color.go:48-77
type BackgroundColorMsg struct{ color.Color }

func (m Model) Init() tea.Cmd {
    return tea.RequestBackgroundColor()
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.BackgroundColorMsg:
        m.styles = newStyles(msg.IsDark())
    }
    return m, nil
}

func newStyles(isDark bool) Styles {
    if isDark {
        return Styles{
            Text: lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")),
        }
    }
    return Styles{
        Text: lipgloss.NewStyle().Foreground(lipgloss.Color("#000000")),
    }
}

Foreground and Cursor Colors

Query other terminal colors:
color.go:17-31
// Request foreground color
func (m model) Init() tea.Cmd {
    return tea.RequestForegroundColor()
}

// Request cursor color  
func (m model) Init() tea.Cmd {
    return tea.RequestCursorColor()
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.ForegroundColorMsg:
        fmt.Println("Foreground:", msg.String())
    case tea.CursorColorMsg:
        fmt.Println("Cursor:", msg.String())
    }
    return m, nil
}

Lip Gloss Layouts

Lip Gloss provides powerful layout primitives:

Horizontal Layout

import "charm.land/lipgloss/v2"

func (m model) View() tea.View {
    left := lipgloss.NewStyle().
        Width(20).
        Render("Left panel")
    
    right := lipgloss.NewStyle().
        Width(20).
        Render("Right panel")
    
    content := lipgloss.JoinHorizontal(lipgloss.Top, left, right)
    return tea.NewView(content)
}

Vertical Layout

header := headerStyle.Render("Header")
body := bodyStyle.Render(m.content)
footer := footerStyle.Render("Footer")

content := lipgloss.JoinVertical(lipgloss.Left, header, body, footer)
return tea.NewView(content)

Borders and Padding

boxStyle := lipgloss.NewStyle().
    Border(lipgloss.RoundedBorder()).
    BorderForeground(lipgloss.Color("#874BFD")).
    Padding(1, 2).
    Width(50)

content := boxStyle.Render("Content inside a box")

Text Formatting

Basic Formatting

boldStyle := lipgloss.NewStyle().Bold(true)
italicStyle := lipgloss.NewStyle().Italic(true)
underlineStyle := lipgloss.NewStyle().Underline(true)
strikethroughStyle := lipgloss.NewStyle().Strikethrough(true)
blinkStyle := lipgloss.NewStyle().Blink(true)

text := boldStyle.Render("Bold") + " " +
        italicStyle.Render("Italic") + " " +
        underlineStyle.Render("Underline")

Alignment

centeredStyle := lipgloss.NewStyle().
    Width(50).
    Align(lipgloss.Center)

leftStyle := lipgloss.NewStyle().
    Width(50).
    Align(lipgloss.Left)

rightStyle := lipgloss.NewStyle().
    Width(50).
    Align(lipgloss.Right)

Adaptive Colors

Lip Gloss supports adaptive colors that change based on background:
adaptiveStyle := lipgloss.NewStyle().
    Foreground(lipgloss.AdaptiveColor{
        Light: "#000000",
        Dark:  "#FFFFFF",
    })

Color Rendering

Bubble Tea uses colorprofile.Writer to automatically downsample colors:
options.go:148-157
import "github.com/charmbracelet/colorprofile"

// Set custom color profile
func WithColorProfile(profile colorprofile.Profile) ProgramOption {
    return func(p *Program) {
        p.profile = &profile
    }
}
This ensures your 24-bit colors work correctly on terminals with limited color support.

Best Practices

Lip Gloss provides a declarative API that’s easier to maintain than raw ANSI codes:
// Good: Declarative and maintainable
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FF0000"))

// Avoid: Hard to read and maintain
text := "\x1b[1;31mRed Bold Text\x1b[0m"
Create style variables at package level:
var (
    titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7D56F4"))
    errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000"))
)
Support both light and dark terminals:
style := lipgloss.NewStyle().
    Foreground(lipgloss.AdaptiveColor{
        Light: "#000000",
        Dark:  "#FFFFFF",
    })
Don’t force TrueColor unless necessary:
// Let Bubble Tea detect automatically (preferred)
p := tea.NewProgram(model{})

// Only force when needed (e.g., testing)
p := tea.NewProgram(model{}, tea.WithColorProfile(colorprofile.Ascii))

Common Patterns

Status Bar

statusStyle := lipgloss.NewStyle().
    Background(lipgloss.Color("#7D56F4")).
    Foreground(lipgloss.Color("#FFFFFF")).
    Padding(0, 1).
    Width(m.width)

status := statusStyle.Render("Ready")

Table Layout

headerStyle := lipgloss.NewStyle().
    Bold(true).
    Background(lipgloss.Color("#7D56F4")).
    Foreground(lipgloss.Color("#FFFFFF")).
    Padding(0, 1)

cellStyle := lipgloss.NewStyle().
    Padding(0, 1).
    Width(20)

header1 := headerStyle.Render("Name")
header2 := headerStyle.Render("Value")

headerRow := lipgloss.JoinHorizontal(lipgloss.Top, header1, header2)

Progress Indicators

progressStyle := lipgloss.NewStyle().
    Background(lipgloss.Color("#7D56F4")).
    Foreground(lipgloss.Color("#FFFFFF"))

emptyStyle := lipgloss.NewStyle().
    Background(lipgloss.Color("#333333"))

filled := progressStyle.Render(strings.Repeat(" ", progress))
empty := emptyStyle.Render(strings.Repeat(" ", total-progress))

bar := "[" + filled + empty + "]"

Build docs developers (and LLMs) love