Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build terminal user interfaces with Go and Bubbletea framework. Use for creating TUI apps with the Elm architecture, dual-pane layouts, accordion modes, mouse/keyboard handling, Lipgloss styling, and reusable components. Includes production-ready templates, effects library, and battle-tested layout patterns from real projects.
.claude/skills/heidihowilson-bubbletea/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 95% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 63% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 40% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 35% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 56% | 0% |
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:
gofunc (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 }
go// 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
go// 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:
gofunc (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:
go// BAD: Can cause misalignment panelStyle := lipgloss.NewStyle(). Border(border). Height(height) // Don't do this!
go// 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:
yamltheme: "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.v3Optional (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 finderAll reference files are loaded progressively as needed:
Follow these patterns and you'll avoid 90% of TUI layout bugs.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | pass→pass | 11,488 | 7,559 | -34% | 1 | 1 | 0% | 2,280 | 3,649 | +60% | 0 | 0 | — |
case-03 | fail→pass | 13,013 | 10,619 | -18% | 1 | 1 | 0% | 2,178 | 4,250 | +95% | 0 | 0 | — |
case-01 | fail→fail | 17,509 | 15,566 | -11% | 1 | 1 | 0% | 3,772 | 5,815 | +54% | 0 | 0 | — |
case-02 | pass→pass | 7,296 | 4,487 | -39% | 1 | 1 | 0% | 1,437 | 3,005 | +109% | 0 | 0 | — |
case-04 | pass→pass | 17,148 | 8,143 | -53% | 1 | 1 | 0% | 3,273 | 3,516 | +7% | 0 | 0 | — |
case-06 | pass→pass | 7,843 | 5,612 | -28% | 1 | 1 | 0% | 1,649 | 3,361 | +104% | 0 | 0 | — |
case-07 | fail→pass | 12,963 | 7,837 | -40% | 1 | 1 | 0% | 2,341 | 3,805 | +63% | 0 | 0 | — |
case-08 | pass→pass | 10,680 | 3,326 | -69% | 1 | 1 | 0% | 2,077 | 2,808 | +35% | 0 | 0 | — |
case-09 | fail→pass | 14,675 | 11,984 | -18% | 1 | 1 | 0% | 3,026 | 4,227 | +40% | 0 | 0 | — |
case-10 | pass→pass | 13,965 | 7,826 | -44% | 1 | 1 | 0% | 2,624 | 3,712 | +41% | 0 | 0 | — |
case-11 | pass→pass | 5,522 | 4,260 | -23% | 1 | 1 | 0% | 1,080 | 2,934 | +172% | 0 | 0 | — |
case-12 | pass→pass | 16,730 | 11,266 | -33% | 1 | 1 | 0% | 3,198 | 4,400 | +38% | 0 | 0 | — |
case-13 | fail→fail | 17,290 | 18,510 | +7% | 1 | 1 | 0% | 2,915 | 5,239 | +80% | 0 | 0 | — |
case-14 | pass→pass | 2,905 | 2,547 | -12% | 1 | 1 | 0% | 579 | 2,657 | +359% | 0 | 0 | — |
case-15 | fail→pass | 11,294 | 5,235 | -54% | 1 | 1 | 0% | 2,285 | 3,093 | +35% | 0 | 0 | — |
case-16 | fail→fail | 16,617 | 5,235 | -68% | 1 | 1 | 0% | 3,162 | 3,133 | -1% | 0 | 0 | — |
case-17 | fail→pass | 11,216 | 8,034 | -28% | 1 | 1 | 0% | 2,509 | 3,906 | +56% | 0 | 0 | — |
case-18 | fail→pass | 12,950 | 6,702 | -48% | 1 | 1 | 0% | 2,482 | 3,412 | +37% | 0 | 0 | — |
case-19 | fail→pass | 12,981 | 2,055 | -84% | 1 | 1 | 0% | 2,060 | 2,497 | +21% | 0 | 0 | — |
case-20 | pass→pass | 6,995 | 5,273 | -25% | 1 | 1 | 0% | 1,406 | 3,179 | +126% | 0 | 0 | — |
case-21 | pass→pass | 7,619 | 5,868 | -23% | 1 | 1 | 0% | 1,531 | 3,272 | +114% | 0 | 0 | — |
case-22 | pass→pass | 4,652 | 3,050 | -34% | 1 | 1 | 0% | 940 | 2,675 | +185% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +32 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.