Build terminal user interfaces with Go and the Bubbletea framework. Use it to create TUI apps with the Elm architecture, dual-pane layouts, accordion modes, mouse and keyboard handling, Lipgloss styling, and reusable components. Includes production-ready templates, an effects library, and battle-tested layout patterns derived from real projects.
Production-ready skill for building beautiful terminal user interfaces with Go, Bubbletea, and Lipgloss.
Use this skill when:
CRITICAL: Before implementing ANY layout, consult references/golden-rules.md for the 4 Golden Rules. These rules prevent the most common and frustrating TUI layout bugs.
Full details and examples in references/golden-rules.md.
This project includes a production-ready template system. When this skill is bundled with a new project (via new_project.sh), use the existing template structure as the starting point.
All new projects follow this architecture:
your-app/
├── main.go # Entry point (minimal, ~21 lines)
├── types.go # Type definitions, structs, enums
├── model.go # Model initialization & layout calculation
├── update.go # Message dispatcher
├── update_keyboard.go # Keyboard handling
├── update_mouse.go # Mouse handling
├── view.go # View rendering & layouts
├── styles.go # Lipgloss style definitions
├── config.go # Configuration management
└── .claude/skills/bubbletea/ # This skill (bundled)
main.go minimal (entry point only, ~21 lines)types.go (structs, enums, constants)See references/components.md for the complete catalog of reusable components:
Beautiful physics-based animations available in the template:
See references/effects.md for usage examples and integration patterns.
When implementing layouts, follow this sequence:
func (m model) calculateLayout() (int, int) {
contentWidth := m.width
contentHeight := m.height
// Subtract UI elements
if m.config.UI.ShowTitle {
contentHeight -= 3 // title bar (3 lines)
}
if m.config.UI.ShowStatus {
contentHeight -= 1 // status bar
}
// CRITICAL: Account for panel borders
contentHeight -= 2 // top + bottom borders
return contentWidth, contentHeight
}
// Calculate weights based on focus/accordion mode
leftWeight, rightWeight := 1, 1
if m.accordionMode && m.focusedPanel == "left" {
leftWeight = 2 // Focused panel gets 2x weight
}
// Calculate actual widths from weights
totalWeight := leftWeight + rightWeight
leftWidth := (availableWidth * leftWeight) / totalWeight
rightWidth := availableWidth - leftWidth
// Calculate max text width to prevent wrapping
maxTextWidth := panelWidth - 4 // -2 borders, -2 padding
// Truncate ALL text before rendering
title = truncateString(title, maxTextWidth)
subtitle = truncateString(subtitle, maxTextWidth)
func truncateString(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen-1] + "…"
}
Always check layout mode before processing mouse events:
func (m model) handleLeftClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
if m.shouldUseVerticalStack() {
// Vertical stack mode: use Y coordinates
topHeight, _ := m.calculateVerticalStackLayout()
relY := msg.Y - contentStartY
if relY < topHeight {
m.focusedPanel = "left" // Top panel
} else {
m.focusedPanel = "right" // Bottom panel
}
} else {
// Side-by-side mode: use X coordinates
leftWidth, _ := m.calculateDualPaneLayout()
if msg.X < leftWidth {
m.focusedPanel = "left"
} else {
m.focusedPanel = "right"
}
}
return m, nil
}
See references/troubleshooting.md for detailed solutions to common issues:
// BAD: Can cause misalignment
panelStyle := lipgloss.NewStyle().
Border(border).
Height(height) // Don't do this!
// GOOD: Fill content lines to exact height
for len(lines) < innerHeight {
lines = append(lines, "")
}
panelStyle := lipgloss.NewStyle().Border(border)
When panels don't align or render incorrectly:
See references/troubleshooting.md for the complete debugging decision tree.
All projects support YAML configuration with hot-reload:
theme: "dark"
keybindings: "default"
layout:
type: "dual_pane"
split_ratio: 0.5
accordion_mode: true
ui:
show_title: true
show_status: true
mouse_enabled: true
show_icons: true
Configuration files are loaded from:
~/.config/your-app/config.yaml (user config)./config.yaml (local override)Required:
github.com/charmbracelet/bubbletea
github.com/charmbracelet/lipgloss
github.com/charmbracelet/bubbles
gopkg.in/yaml.v3
Optional (uncomment in go.mod as needed):
github.com/charmbracelet/glamour # Markdown rendering
github.com/charmbracelet/huh # Forms
github.com/alecthomas/chroma/v2 # Syntax highlighting
github.com/evertras/bubble-table # Interactive tables
github.com/koki-develop/go-fzf # Fuzzy finder
All reference files are loaded progressively as needed:
Follow these patterns and you'll avoid 90% of TUI layout bugs.
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer