Charm Bracelet TUI Stack
The Charm Bracelet ecosystem is a collection of Go libraries for building beautiful terminal UIs. They are designed to work together but can also be used independently.
Current versions: All core Charm libs are on v2 as of early 2026 (import paths use charm.land/<lib>/v2).
The Stack at a Glance
┌─────────────────────────────────────────────────────────────┐
│ Your TUI Application │
├──────────────────────────┬──────────────────────────────────┤
│ huh (forms/prompts) │ Bubbles (pre-built components) │
├──────────────────────────┴──────────────────────────────────┤
│ BubbleTea (MVU framework) │
├──────────────────┬──────────────┬───────────────────────────┤
│ Lip Gloss │ Glamour │ Harmonica │
│ (styling) │ (markdown) │ (animation) │
├──────────────────┴──────────────┴───────────────────────────┤
│ BubbleZone (mouse tracking) │ ntcharts (charts) │
└─────────────────────────────────────────────────────────────┘
Library Roles
| Library |
Role |
Import |
| BubbleTea |
MVU framework — the backbone of every app |
charm.land/bubbletea/v2 |
| Bubbles |
Pre-built TUI components (inputs, lists, etc.) |
charm.land/bubbles/v2 (note: check go.dev for exact path) |
| Lip Gloss |
Styling, layout, colors, borders, tables, trees |
charm.land/lipgloss/v2 |
| Glamour |
Stylesheet-based Markdown renderer |
github.com/charmbracelet/glamour |
| Huh |
Terminal forms and interactive prompts |
charm.land/huh/v2 |
| Harmonica |
Physics-based spring animations |
github.com/charmbracelet/harmonica |
| BubbleZone |
Mouse event tracking across components |
github.com/lrstanley/bubblezone/v2 |
| ntcharts |
Terminal charts (bar, line, sparkline, heatmap, etc.) |
github.com/NimbleMarkets/ntcharts |
Quick Reference: Which Library for What?
- Need a TUI framework? → BubbleTea (always the foundation)
- Need forms / user input collection? → Huh (or Bubbles textinput/textarea)
- Need styled output / layouts? → Lip Gloss
- Need to render Markdown? → Glamour
- Need pre-built components? → Bubbles (spinner, progress, list, table, viewport, filepicker...)
- Need smooth animations? → Harmonica
- Need mouse click detection per-component? → BubbleZone
- Need charts / graphs? → ntcharts
How They Work Together
BubbleTea + Lip Gloss (the core combo)
BubbleTea handles the update loop; Lip Gloss handles the rendering. Your View() method returns a tea.View built by rendering Lip Gloss styles:
func (m model) View() tea.View {
title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FF75B7")).Render("My App")
body := lipgloss.NewStyle().Padding(1, 2).Render("Hello world")
return tea.NewView(lipgloss.JoinVertical(lipgloss.Left, title, body))
}
BubbleTea + Huh (embedded forms)
huh.Form is a tea.Model, so embed it directly:
type Model struct {
form *huh.Form
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
form, cmd := m.form.Update(msg)
if f, ok := form.(*huh.Form); ok { m.form = f }
return m, cmd
}
func (m Model) View() string { return m.form.View() }
BubbleTea + BubbleZone (mouse support)
Wrap your root View() in zone.Scan(), mark individual areas with zone.Mark(id, content), check zone.Get(id).InBounds(mouseMsg) in Update().
BubbleTea + ntcharts (charts)
ntcharts components are standalone (they have their own Draw()/View() methods), so embed them in your model and call them during Update() and View().
Getting Started
# Core framework
go get charm.land/bubbletea/v2
# Styling
go get charm.land/lipgloss/v2
# Pre-built components
go get github.com/charmbracelet/bubbles
# Forms
go get charm.land/huh/v2
# Markdown
go get github.com/charmbracelet/glamour
# Animations
go get github.com/charmbracelet/harmonica
# Mouse tracking
go get github.com/lrstanley/bubblezone/v2
# Charts
go get github.com/NimbleMarkets/ntcharts
Minimal BubbleTea App (v2 API)
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
)
type model struct{ count int }
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.KeyPressMsg:
switch msg.String() {
case "q", "ctrl+c": return m, tea.Quit
case "up": m.count++
case "down": m.count--
}
}
return m, nil
}
func (m model) View() tea.View {
return tea.NewView(fmt.Sprintf("Count: %d\n\nPress q to quit.", m.count))
}
func main() {
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Reference Files
Read the relevant reference file(s) for deep implementation details:
references/bubbletea.md — BubbleTea framework: MVU lifecycle, commands, messages, v2 View API
references/lipgloss.md — Lip Gloss: styles, colors, layouts, tables, trees, lists, compositing
references/bubbles.md — Bubbles components: spinner, textinput, textarea, list, table, viewport, progress, filepicker, timer, help
references/glamour.md — Glamour: markdown rendering, themes, custom styles
references/huh.md — Huh: forms, fields, dynamic forms, BubbleTea integration, spinner
references/harmonica.md — Harmonica: spring animations, damping ratios, BubbleTea integration
references/bubblezone.md — BubbleZone: mouse zones, InBounds, global vs local manager
references/ntcharts.md — ntcharts: canvas, bar, line, sparkline, heatmap, OHLC, time series, waveline
When to Read Which
| Task |
Reference(s) |
| Building a new TUI app from scratch |
bubbletea.md → lipgloss.md |
| Adding forms/prompts |
huh.md |
| Adding pre-built widgets |
bubbles.md |
| Rendering markdown |
glamour.md |
| Adding animations |
harmonica.md |
| Mouse click detection |
bubblezone.md |
| Charts and data visualization |
ntcharts.md |
| Complex layouts / styling |
lipgloss.md |
Common Gotchas
- v2 import paths: Core Charm libs moved to
charm.land/<lib>/v2 in 2025-2026. github.com/charmbracelet/* paths still work for some (glamour, harmonica), but prefer charm.land for bubbletea, lipgloss, huh.
- BubbleTea v2 View:
View() now returns tea.View (not string). Use tea.NewView(str) to wrap string content.
- AltScreen for BubbleZone: BubbleZone requires alt-screen mode — set
view.AltScreen = true in your root view.
- Dynamic hex colors in Lip Gloss: Use
lipgloss.Color("#RRGGBB") — Lip Gloss will automatically downsample for lower-color terminals.
- Lip Gloss Width vs len(): Always use
lipgloss.Width(str) instead of len() — ANSI codes inflate len().
- BubbleZone zone.Scan() at root only: Only wrap your root model's view output with
zone.Scan().
- ntcharts: Call
.Draw() before .View() to rasterize the chart each frame.
1---2name: charm-bracelet-tui3description: Complete guide for building beautiful, interactive Go TUI (terminal UI) applications using the Charm Bracelet ecosystem. ALWAYS use this skill when the user mentions: bubbletea, bubbles, lipgloss, glamour, huh, harmonica, bubblezone, ntcharts, TUI in Go, terminal user interface in Go, terminal app with Go, CLI with interactive UI, charm.sh, charm.land, or wants to build any kind of interactive terminal application. Also trigger when building Go CLI tools that need forms, menus, progress bars, spinners, charts, markdown rendering, styled output, or mouse support. This covers the full stack from framework to styling to widgets to animations.4---56# Charm Bracelet TUI Stack78The Charm Bracelet ecosystem is a collection of Go libraries for building beautiful terminal UIs. They are designed to work together but can also be used independently.910**Current versions:** All core Charm libs are on **v2** as of early 2026 (import paths use `charm.land/<lib>/v2`).1112## The Stack at a Glance1314```15┌─────────────────────────────────────────────────────────────┐16│ Your TUI Application │17├──────────────────────────┬──────────────────────────────────┤18│ huh (forms/prompts) │ Bubbles (pre-built components) │19├──────────────────────────┴──────────────────────────────────┤20│ BubbleTea (MVU framework) │21├──────────────────┬──────────────┬───────────────────────────┤22│ Lip Gloss │ Glamour │ Harmonica │23│ (styling) │ (markdown) │ (animation) │24├──────────────────┴──────────────┴───────────────────────────┤25│ BubbleZone (mouse tracking) │ ntcharts (charts) │26└─────────────────────────────────────────────────────────────┘27```2829## Library Roles3031| Library | Role | Import |32|---------|------|--------|33| **BubbleTea** | MVU framework — the backbone of every app | `charm.land/bubbletea/v2` |34| **Bubbles** | Pre-built TUI components (inputs, lists, etc.) | `charm.land/bubbles/v2` (note: check go.dev for exact path) |35| **Lip Gloss** | Styling, layout, colors, borders, tables, trees | `charm.land/lipgloss/v2` |36| **Glamour** | Stylesheet-based Markdown renderer | `github.com/charmbracelet/glamour` |37| **Huh** | Terminal forms and interactive prompts | `charm.land/huh/v2` |38| **Harmonica** | Physics-based spring animations | `github.com/charmbracelet/harmonica` |39| **BubbleZone** | Mouse event tracking across components | `github.com/lrstanley/bubblezone/v2` |40| **ntcharts** | Terminal charts (bar, line, sparkline, heatmap, etc.) | `github.com/NimbleMarkets/ntcharts` |4142## Quick Reference: Which Library for What?4344- **Need a TUI framework?** → BubbleTea (always the foundation)45- **Need forms / user input collection?** → Huh (or Bubbles textinput/textarea)46- **Need styled output / layouts?** → Lip Gloss47- **Need to render Markdown?** → Glamour48- **Need pre-built components?** → Bubbles (spinner, progress, list, table, viewport, filepicker...)49- **Need smooth animations?** → Harmonica50- **Need mouse click detection per-component?** → BubbleZone51- **Need charts / graphs?** → ntcharts5253---5455## How They Work Together5657### BubbleTea + Lip Gloss (the core combo)58BubbleTea handles the update loop; Lip Gloss handles the rendering. Your `View()` method returns a `tea.View` built by rendering Lip Gloss styles:5960```go61func (m model) View() tea.View {62 title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FF75B7")).Render("My App")63 body := lipgloss.NewStyle().Padding(1, 2).Render("Hello world")64 return tea.NewView(lipgloss.JoinVertical(lipgloss.Left, title, body))65}66```6768### BubbleTea + Huh (embedded forms)69`huh.Form` is a `tea.Model`, so embed it directly:7071```go72type Model struct {73 form *huh.Form74}75func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {76 form, cmd := m.form.Update(msg)77 if f, ok := form.(*huh.Form); ok { m.form = f }78 return m, cmd79}80func (m Model) View() string { return m.form.View() }81```8283### BubbleTea + BubbleZone (mouse support)84Wrap your root `View()` in `zone.Scan()`, mark individual areas with `zone.Mark(id, content)`, check `zone.Get(id).InBounds(mouseMsg)` in `Update()`.8586### BubbleTea + ntcharts (charts)87ntcharts components are standalone (they have their own `Draw()`/`View()` methods), so embed them in your model and call them during `Update()` and `View()`.8889---9091## Getting Started9293```bash94# Core framework95go get charm.land/bubbletea/v29697# Styling98go get charm.land/lipgloss/v299100# Pre-built components101go get github.com/charmbracelet/bubbles102103# Forms104go get charm.land/huh/v2105106# Markdown107go get github.com/charmbracelet/glamour108109# Animations110go get github.com/charmbracelet/harmonica111112# Mouse tracking113go get github.com/lrstanley/bubblezone/v2114115# Charts116go get github.com/NimbleMarkets/ntcharts117```118119---120121## Minimal BubbleTea App (v2 API)122123```go124package main125126import (127 "fmt"128 "os"129 tea "charm.land/bubbletea/v2"130)131132type model struct{ count int }133134func (m model) Init() tea.Cmd { return nil }135136func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {137 switch msg := msg.(type) {138 case tea.KeyPressMsg:139 switch msg.String() {140 case "q", "ctrl+c": return m, tea.Quit141 case "up": m.count++142 case "down": m.count--143 }144 }145 return m, nil146}147148func (m model) View() tea.View {149 return tea.NewView(fmt.Sprintf("Count: %d\n\nPress q to quit.", m.count))150}151152func main() {153 p := tea.NewProgram(model{})154 if _, err := p.Run(); err != nil {155 fmt.Fprintln(os.Stderr, err)156 os.Exit(1)157 }158}159```160161---162163## Reference Files164165Read the relevant reference file(s) for deep implementation details:166167- `references/bubbletea.md` — BubbleTea framework: MVU lifecycle, commands, messages, v2 View API168- `references/lipgloss.md` — Lip Gloss: styles, colors, layouts, tables, trees, lists, compositing169- `references/bubbles.md` — Bubbles components: spinner, textinput, textarea, list, table, viewport, progress, filepicker, timer, help170- `references/glamour.md` — Glamour: markdown rendering, themes, custom styles171- `references/huh.md` — Huh: forms, fields, dynamic forms, BubbleTea integration, spinner172- `references/harmonica.md` — Harmonica: spring animations, damping ratios, BubbleTea integration173- `references/bubblezone.md` — BubbleZone: mouse zones, InBounds, global vs local manager174- `references/ntcharts.md` — ntcharts: canvas, bar, line, sparkline, heatmap, OHLC, time series, waveline175176### When to Read Which177178| Task | Reference(s) |179|------|-------------|180| Building a new TUI app from scratch | `bubbletea.md` → `lipgloss.md` |181| Adding forms/prompts | `huh.md` |182| Adding pre-built widgets | `bubbles.md` |183| Rendering markdown | `glamour.md` |184| Adding animations | `harmonica.md` |185| Mouse click detection | `bubblezone.md` |186| Charts and data visualization | `ntcharts.md` |187| Complex layouts / styling | `lipgloss.md` |188189---190191## Common Gotchas192193- **v2 import paths**: Core Charm libs moved to `charm.land/<lib>/v2` in 2025-2026. `github.com/charmbracelet/*` paths still work for some (glamour, harmonica), but prefer `charm.land` for bubbletea, lipgloss, huh.194- **BubbleTea v2 View**: `View()` now returns `tea.View` (not `string`). Use `tea.NewView(str)` to wrap string content.195- **AltScreen for BubbleZone**: BubbleZone requires alt-screen mode — set `view.AltScreen = true` in your root view.196- **Dynamic hex colors in Lip Gloss**: Use `lipgloss.Color("#RRGGBB")` — Lip Gloss will automatically downsample for lower-color terminals.197- **Lip Gloss Width vs len()**: Always use `lipgloss.Width(str)` instead of `len()` — ANSI codes inflate `len()`.198- **BubbleZone zone.Scan() at root only**: Only wrap your root model's view output with `zone.Scan()`.199- **ntcharts**: Call `.Draw()` before `.View()` to rasterize the chart each frame.