charm-huh
Build interactive terminal forms and prompts in Go with huh - input, select, confirm, multiselect, validation, theming. Use when building Go terminal forms, huh, interactive Go prompts, or form fields with validation. NOT for shell script prompts (use gum).
Best use case
charm-huh is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Build interactive terminal forms and prompts in Go with huh - input, select, confirm, multiselect, validation, theming. Use when building Go terminal forms, huh, interactive Go prompts, or form fields with validation. NOT for shell script prompts (use gum).
Teams using charm-huh should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/charm-huh/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How charm-huh Compares
| Feature / Agent | charm-huh | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Build interactive terminal forms and prompts in Go with huh - input, select, confirm, multiselect, validation, theming. Use when building Go terminal forms, huh, interactive Go prompts, or form fields with validation. NOT for shell script prompts (use gum).
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# charmbracelet/huh
Interactive terminal forms and prompts for Go. Built on Bubble Tea.
Import: `charm.land/huh/v2`
## Quick Start
```go
package main
import (
"fmt"
"log"
"charm.land/huh/v2"
)
func main() {
var name string
var confirm bool
err := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("What's your name?").
Value(&name).
Validate(huh.ValidateNotEmpty()),
huh.NewConfirm().
Title("Ready?").
Value(&confirm),
),
).Run()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Hello, %s!\n", name)
}
```
Single field shorthand (no Form/Group wrapper needed):
```go
var name string
huh.NewInput().Title("Name?").Value(&name).Run()
```
## Core API
### Architecture
`Form` > `Group` (pages) > `Field` (inputs)
Groups are displayed one at a time. The form advances to the next group when all fields in the current group pass validation.
### Form
```go
form := huh.NewForm(groups...*Group) *Form
```
Key methods:
| Method | Purpose |
|--------|---------|
| `.Run()` | Block and run the form |
| `.RunWithContext(ctx)` | Run with context (supports cancellation) |
| `.WithTheme(theme)` | Set theme |
| `.WithWidth(w)` / `.WithHeight(h)` | Set dimensions |
| `.WithAccessible(bool)` | Screen reader mode |
| `.WithShowHelp(bool)` | Toggle help bar |
| `.WithShowErrors(bool)` | Toggle error display |
| `.WithTimeout(duration)` | Auto-cancel after duration |
| `.WithLayout(layout)` | Set group layout |
| `.WithKeyMap(keymap)` | Custom keybindings |
| `.WithOutput(w)` / `.WithInput(r)` | Custom IO |
Retrieve values by key after completion:
```go
form.GetString("key")
form.GetInt("key")
form.GetBool("key")
form.Get("key") // any
```
Form states: `huh.StateNormal`, `huh.StateCompleted`, `huh.StateAborted`
Errors: `huh.ErrUserAborted` (ctrl+c), `huh.ErrTimeout`
### Group
```go
group := huh.NewGroup(fields ...Field) *Group
```
| Method | Purpose |
|--------|---------|
| `.Title(s)` / `.Description(s)` | Group header |
| `.WithHide(bool)` | Skip this group |
| `.WithHideFunc(func() bool)` | Conditionally skip group |
| `.WithShowHelp(bool)` | Toggle help for group |
### Field Types
Every field supports: `.Title(s)`, `.Description(s)`, `.Key(s)`, `.Value(&v)`, `.Validate(fn)`, `.Run()`.
Dynamic variants exist for most properties: `.TitleFunc(fn, binding)`, `.DescriptionFunc(fn, binding)`, etc.
#### Input
Single line text. Type: `string`.
```go
huh.NewInput().
Title("Email").
Placeholder("you@example.com").
Prompt("> ").
CharLimit(100).
Suggestions([]string{"gmail.com", "outlook.com"}).
EchoMode(huh.EchoModePassword). // or EchoModeNone
Inline(true). // title and input on same line
Validate(huh.ValidateNotEmpty()).
Value(&email)
```
#### Text
Multi-line textarea. Type: `string`.
```go
huh.NewText().
Title("Description").
Lines(5).
CharLimit(500).
Placeholder("Enter details...").
ShowLineNumbers(true).
Editor("vim"). // external editor support (ctrl+e)
EditorExtension("md").
ExternalEditor(false). // disable external editor
Value(&description)
```
#### Select
Pick one from a list. Generic: `Select[T comparable]`.
```go
huh.NewSelect[string]().
Title("Country").
Options(
huh.NewOption("United States", "US"),
huh.NewOption("Canada", "CA"),
).
Height(8). // scrollable if options exceed height
Inline(true). // horizontal left/right navigation
Filtering(true). // start with filter active
Value(&country)
```
Shorthand for simple options:
```go
Options(huh.NewOptions("Warrior", "Mage", "Rogue")...)
```
#### MultiSelect
Pick zero or more. Generic: `MultiSelect[T comparable]`.
```go
huh.NewMultiSelect[string]().
Title("Toppings").
Options(
huh.NewOption("Lettuce", "lettuce").Selected(true),
huh.NewOption("Tomato", "tomato"),
huh.NewOption("Cheese", "cheese"),
).
Limit(3).
Height(6).
Filterable(false). // disable "/" filter
Value(&toppings)
```
Space to toggle, `a` to select all (when no limit), `/` to filter.
#### Confirm
Yes/No. Type: `bool`.
```go
huh.NewConfirm().
Title("Continue?").
Affirmative("Yes!").
Negative("No way").
Inline(true).
Value(&ok)
```
Keys: `h`/`l` or left/right to toggle, `y` to accept, `n` to reject.
#### Note
Display-only. Not interactive by default (auto-skipped).
```go
huh.NewNote().
Title("Welcome").
Description("This form collects your _preferences_.").
Height(10).
Next(true). // show a "Next" button, makes it interactive
NextLabel("Continue")
```
Description supports basic markdown: `_italic_`, `*bold*`, `` `code` ``.
## Common Patterns
### Multi-Step Forms
Groups act as pages. Users navigate forward/back between them.
```go
huh.NewForm(
huh.NewGroup(/* step 1 fields */).Title("Step 1"),
huh.NewGroup(/* step 2 fields */).Title("Step 2"),
huh.NewGroup(/* step 3 fields */).Title("Step 3"),
).Run()
```
### Conditional Groups
Hide groups based on previous answers:
```go
var wantExtras bool
huh.NewForm(
huh.NewGroup(
huh.NewConfirm().Title("Want extras?").Value(&wantExtras),
),
huh.NewGroup(
huh.NewInput().Title("Extra details").Value(&details),
).WithHideFunc(func() bool { return !wantExtras }),
).Run()
```
### Dynamic Fields
Use `*Func` variants to recompute properties when bindings change. Pass a pointer to the bound variable.
```go
var country string
huh.NewSelect[string]().
Value(&state).
TitleFunc(func() string {
if country == "Canada" { return "Province" }
return "State"
}, &country).
OptionsFunc(func() []huh.Option[string] {
return huh.NewOptions(statesByCountry[country]...)
}, &country)
```
The binding (`&country`) tells huh when to recompute. Results are cached per binding hash.
### Validation
Built-in validators:
```go
huh.ValidateNotEmpty()
huh.ValidateMinLength(3)
huh.ValidateMaxLength(100)
huh.ValidateLength(3, 100) // min and max
huh.ValidateOneOf("a", "b", "c")
```
Custom validation:
```go
.Validate(func(s string) error {
if !strings.Contains(s, "@") {
return fmt.Errorf("must be a valid email")
}
return nil
})
```
Validation runs on blur (when leaving a field). Forms block progression if any field in the current group has errors.
### Theming
Built-in themes: `ThemeCharm` (default), `ThemeDracula`, `ThemeCatppuccin`, `ThemeBase16`, `ThemeDefault`.
```go
form.WithTheme(huh.ThemeFunc(huh.ThemeDracula))
```
Custom theme - implement the `Theme` interface:
```go
type Theme interface {
Theme(isDark bool) *Styles
}
```
Or use `ThemeFunc`:
```go
form.WithTheme(huh.ThemeFunc(func(isDark bool) *huh.Styles {
s := huh.ThemeCharm(isDark) // start from a base
s.Focused.Title = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212"))
return s
}))
```
### Layouts
```go
form.WithLayout(huh.LayoutDefault) // one group at a time (default)
form.WithLayout(huh.LayoutStack) // all groups stacked vertically
form.WithLayout(huh.LayoutColumns(2)) // groups in 2 columns
form.WithLayout(huh.LayoutGrid(2, 3)) // 2 rows, 3 columns
```
### Accessibility
```go
accessible := os.Getenv("ACCESSIBLE") != ""
form.WithAccessible(accessible)
```
When `TERM=dumb`, accessible mode activates automatically. Replaces TUI with plain text prompts. Timeout is not supported in accessible mode.
### Spinner
Separate package for loading indicators after form submission:
```go
import "charm.land/huh/v2/spinner"
err := spinner.New().
Title("Processing...").
Action(func() { /* do work */ }).
Run()
```
Or with context:
```go
go doWork()
spinner.New().Title("Working...").Context(ctx).Run()
```
## Integration: Standalone vs Bubble Tea
### Standalone
Call `.Run()` on a form or individual field. Blocks until complete.
```go
form.Run()
// or
huh.NewInput().Title("Name?").Value(&name).Run()
```
### Embedded in Bubble Tea
`*huh.Form` implements `tea.Model`. Use it as a component in your Bubble Tea app.
```go
type Model struct {
form *huh.Form
}
func NewModel() Model {
return Model{
form: huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Key("class").
Options(huh.NewOptions("Warrior", "Mage", "Rogue")...).
Title("Choose your class"),
),
),
}
}
func (m Model) Init() tea.Cmd {
return m.form.Init()
}
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
}
if m.form.State == huh.StateCompleted {
return m, tea.Quit
}
return m, cmd
}
func (m Model) View() string {
if m.form.State == huh.StateCompleted {
return fmt.Sprintf("You picked: %s", m.form.GetString("class"))
}
return m.form.View()
}
```
Key differences when embedded:
- Do NOT call `.Run()`, use Init/Update/View cycle instead
- Set `SubmitCmd` and `CancelCmd` if you want custom behavior on form completion
- Use `.Key("name")` on fields, retrieve with `form.GetString("name")`
- Check `form.State` to know when the form is done
- Type assert the Update result: `form.(*huh.Form)`
Navigation methods available for programmatic control:
- `form.NextGroup()`, `form.PrevGroup()`
- `form.NextField()`, `form.PrevField()`
- `form.GetFocusedField()`
## Common Mistakes
1. **Forgetting `.Value(&v)`** - Without it, answers go nowhere. The field uses an internal `EmbeddedAccessor` that you cannot read after form completes unless you use `.Key()` + `form.GetString()`.
2. **Using `.Run()` inside Bubble Tea** - Never call `.Run()` on an embedded form. Use the Init/Update/View pattern.
3. **Missing type parameter on Select/MultiSelect** - `huh.NewSelect[string]()` not `huh.NewSelect()`. The generic parameter determines the option value type.
4. **Dynamic binding without pointer** - `TitleFunc(fn, country)` will not work. Must be `TitleFunc(fn, &country)` with a pointer so huh can detect changes.
5. **Timeout in accessible mode** - `WithTimeout()` returns `ErrTimeoutUnsupported` in accessible mode. Guard it.
6. **Not handling ErrUserAborted** - `form.Run()` returns `huh.ErrUserAborted` when user presses ctrl+c. Always check the error.
7. **Form outputs to stderr** - By default, the TUI renders to stderr (stdout stays clean for piping). Use `.WithOutput(os.Stdout)` to change.
## Checklist
- [ ] Import `charm.land/huh/v2`
- [ ] Every field that stores data has `.Value(&var)` or `.Key("name")`
- [ ] Custom validators return `nil` on success, `error` on failure
- [ ] Dynamic fields use `*Func` variants with pointer bindings
- [ ] Accessible mode handled via env var or config flag
- [ ] `ErrUserAborted` handled after `.Run()`
- [ ] Embedded forms use Init/Update/View, not `.Run()`
- [ ] Form state checked via `form.State == huh.StateCompleted`Related Skills
charm-vhs
Record terminal sessions as GIF/MP4/WebM from declarative .tape scripts with VHS. Use when creating terminal demos, recording CLI sessions, VHS tape files, or generating terminal GIFs.
charm-ultraviolet
Low-level Go terminal primitives - cell-based rendering, input handling, screen management. Use when building custom Go terminal renderers, ultraviolet, cell buffers, or performance-critical TUI work below Bubble Tea's abstraction level.
charm-pop
Send emails from the terminal with pop - TUI and CLI modes, SMTP and Resend support, attachments. Use when sending email from terminal, pop, CLI email, or piping email content from shell scripts.
charm-lipgloss
CSS-like terminal styling for Go with lipgloss v2 - styles, colors, borders, layout, tables, lists, and trees. Use when styling Go terminal output, lipgloss, terminal layout composition, or building styled tables/lists/trees in Go.
charm-harmonica
Physics-based animation for Go TUIs - damped spring oscillator and projectile motion. Use when adding spring animations, physics-based motion, or smooth transitions to Go terminal apps. No Ease function exists in this library.
charm-gum
Interactive shell script prompts, fuzzy filters, spinners, and styled output with gum. Use when building bash/shell script UIs, gum commands, interactive shell prompts, or CLI script workflows. NOT for Go terminal forms (use huh).
charm-freeze
Generate PNG, SVG, or WebP screenshots of code and terminal output with freeze. Use when screenshotting code, freeze, terminal-to-image, or capturing styled code snippets as images.
charm-fang
Wrap Cobra with styled help, error output, auto versioning, and manpage generation via fang. Use when building Go CLIs with fang, styled Cobra help, or adding lipgloss-rendered help pages to a Go CLI.
charm-ecosystem
Architect's guide to the charmbracelet Go TUI ecosystem - which libraries to combine, dependency hierarchy, integration patterns. Use when choosing charm libraries, planning TUI architecture, combining bubbletea/lipgloss/bubbles/huh, or asking 'which charm library for X'. NOT for specific library API details (use individual charm-* skills).
charm-crush
Architecture patterns from Crush, charmbracelet's production agentic coding CLI built on bubbletea v2, lipgloss v2, bubbles v2, glamour v2, and ultraviolet. Use when building production bubbletea apps, composing charm TUI components at scale, designing agentic CLI tools, implementing streaming LLM UIs, or asking about crush internals. NOT for individual charm library basics.
charm-bubbletea
Build terminal UIs in Go with Bubble Tea v2's Elm Architecture (Model/Update/View). Use when building Go TUI apps, tea.Model, tea.Cmd, Elm architecture, or terminal applications. NOT for pre-built TUI components (use bubbles).
charm-bubbles
Pre-built TUI components for Bubble Tea apps - spinner, text input, textarea, list, table, viewport, paginator, progress bar. Use when adding Go TUI components, bubbles, or terminal widgets to a Bubble Tea app. NOT for the core TUI framework (use bubbletea).