| name | huh-v2 |
| description | Generates, explains, and debugs interactive terminal form and prompt code using Charm's huh v2 library for Go (charm.land/huh/v2). Use when building CLI tools that need user input via forms, single-field prompts, multi-step wizards, dynamic forms that react to earlier answers, accessible prompts for screen readers, or loading spinners. Covers all field types (Input, Text, Select, MultiSelect, Confirm, Note, FilePicker), Form/Group composition, dynamic Func variants, BubbleTea integration, themes, layouts, accessibility mode, keymaps, and the standalone spinner package. Import path: charm.land/huh/v2. |
huh v2 — Terminal Forms & Prompts
Import: charm.land/huh/v2
Spinner subpackage: charm.land/huh/v2/spinner
Docs: https://pkg.go.dev/charm.land/huh/v2
go get charm.land/huh/v2@v2.0.0-20260105203756-d8977490d20c
huh? builds interactive terminal forms from fields grouped into pages. Each field is a Bubble Tea Model — usable standalone via .Run() or embedded in a larger Bubble Tea app.
Quick Start
var name string
err := huh.NewInput().Title("Name?").Value(&name).Run()
Or a full multi-page form:
err := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Language").
Options(huh.NewOptions("Go", "Rust", "Zig")...).
Value(&lang),
),
huh.NewGroup(
huh.NewInput().Title("Name").Value(&name).
Validate(func(s string) error {
if s == "" { return errors.New("required") }
return nil
}),
huh.NewConfirm().Title("Proceed?").Value(&ok),
),
).Run()
if errors.Is(err, huh.ErrUserAborted) { os.Exit(0) }
Field Types
| Field | Constructor | Value type |
|---|
| Single-line text | huh.NewInput() | *string |
| Multi-line text | huh.NewText() | *string |
| Single choice | huh.NewSelect[T]() | *T |
| Multiple choices | huh.NewMultiSelect[T]() | *[]T |
| Yes/No | huh.NewConfirm() | *bool |
| Display text | huh.NewNote() | none |
| File chooser | huh.NewFilePicker() | *string |
Common Field Methods (all fields)
.Title("label")
.TitleFunc(func() string, &bind)
.Description("hint text")
.DescriptionFunc(func() string, &bind)
.Value(&variable)
.Key("fieldKey")
.Validate(func(v T) error)
.Run()
Input Field
huh.NewInput().
Title("Username").
Placeholder("johndoe").
Prompt("> ").
CharLimit(50).
EchoMode(huh.EchoModePassword).
Suggestions([]string{"alice", "bob"}).
SuggestionsFunc(func() []string { ... }, &bind).
Inline(true).
Value(&s)
EchoMode values: EchoModeNormal, EchoModePassword, EchoModeNone
Text Field (multi-line)
huh.NewText().
Title("Bio").
Placeholder("Tell us about yourself...").
Lines(5).
CharLimit(500).
ShowLineNumbers(true).
ExternalEditor(true).
Editor("vim").
EditorExtension("md").
Value(&bio)
Select Field
huh.NewSelect[string]().
Title("Color").
Options(
huh.NewOption("Red", "red"),
huh.NewOption("Blue", "blue").Selected(true),
).
Height(5).
Inline(false).
Validate(func(v string) error { ... }).
Value(&color)
huh.NewSelect[string]().Options(huh.NewOptions("Go", "Rust", "Zig")...)
huh.NewSelect[string]().
OptionsFunc(func() []huh.Option[string] {
return fetchOptions(country)
}, &country).
TitleFunc(func() string { return "State in " + country }, &country)
huh.Hovered() → returns the value currently under the cursor before submit.
MultiSelect Field
huh.NewMultiSelect[string]().
Title("Features").
Options(
huh.NewOption("Auth", "auth").Selected(true),
huh.NewOption("Logging", "logging"),
).
Limit(3).
Filterable(true).
Height(8).
Validate(func(vs []string) error {
if len(vs) == 0 { return errors.New("pick at least one") }
return nil
}).
Value(&features)
Key bindings: space/x toggle, ctrl+a select-all/none, / filter.
Confirm Field
huh.NewConfirm().
Title("Deploy?").
Description("This will restart the server.").
Affirmative("Yes!").
Negative("No.").
Inline(false).
Value(&confirmed)
Key bindings: ←/→ or h/l toggle, y yes, n no.
Note Field (display only)
huh.NewNote().
Title("Welcome").
Description("_italic_ *bold* `code`").
Next(true).
NextLabel("Continue").
Height(10)
FilePicker Field
huh.NewFilePicker().
Title("Config file").
CurrentDirectory(".").
AllowedTypes([]string{".json", ".yaml"}).
Value(&path)
Options Helpers
huh.NewOption("Display Label", actualValue)
huh.NewOption("Active", true).Selected(true)
huh.NewOptions("Go", "Rust", "Zig")
huh.NewOptions(1, 2, 3)
Form & Group
form := huh.NewForm(group1, group2, ...)
form.WithTheme(huh.ThemeFunc(huh.ThemeCharm))
form.WithWidth(80)
form.WithHeight(24)
form.WithAccessible(os.Getenv("ACCESSIBLE") != "")
form.WithShowHelp(true)
form.WithShowErrors(true)
form.WithKeyMap(huh.NewDefaultKeyMap())
form.WithLayout(huh.LayoutDefault)
form.WithTimeout(30 * time.Second)
form.WithOutput(os.Stderr)
group := huh.NewGroup(field1, field2).
Title("Page title").
Description("Page subtitle").
WithHide(true).
WithHideFunc(func() bool { return !showGroup })
Dynamic Forms (Func variants)
All fields support XxxFunc(fn, &binding) for every static setter. The function
re-runs only when the value at &binding changes (pointer equality tracking).
Results are automatically cached.
var country string
huh.NewSelect[string]().
TitleFunc(func() string {
if country == "US" { return "State" }
return "Province"
}, &country).
OptionsFunc(func() []huh.Option[string] {
return fetchRegions(country)
}, &country).
Value(®ion)
Group Hiding (Conditional Pages)
var wantExtras bool
huh.NewForm(
huh.NewGroup(
huh.NewConfirm().Title("Want extras?").Value(&wantExtras),
),
huh.NewGroup(
huh.NewText().Title("Describe extras").Value(&extras),
).WithHideFunc(func() bool { return !wantExtras }),
)
Layouts
form.WithLayout(huh.LayoutDefault)
form.WithLayout(huh.LayoutStack)
form.WithLayout(huh.LayoutColumns(2))
form.WithLayout(huh.LayoutGrid(rows, cols))
Themes
Built-in themes (all accept isDark bool):
huh.ThemeCharm (default)
huh.ThemeDracula
huh.ThemeCatppuccin
huh.ThemeBase16
huh.ThemeBase
form.WithTheme(huh.ThemeFunc(huh.ThemeDracula))
form.WithTheme(huh.ThemeFunc(huh.ThemeCatppuccin))
myTheme := huh.ThemeFunc(func(isDark bool) *huh.Styles {
t := huh.ThemeCharm(isDark)
t.Focused.Title = t.Focused.Title.Foreground(lipgloss.Color("#FF0000"))
return t
})
form.WithTheme(myTheme)
Accessibility Mode
accessible := os.Getenv("ACCESSIBLE") != ""
form.WithAccessible(accessible)
Standalone Spinner
import "charm.land/huh/v2/spinner"
err := spinner.New().
Title("Processing...").
Action(func() { doWork() }).
Run()
err := spinner.New().
Title("Fetching data...").
ActionWithErr(func(ctx context.Context) error {
return fetchData(ctx)
}).
Run()
go doWork()
err := spinner.New().
Type(spinner.Line).
Title("Working...").
Context(ctx).
Run()
BubbleTea Integration
huh.Form implements tea.Model. Use .Init(), .Update(), .View() directly:
type Model struct {
form *huh.Form
}
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 {
class := m.form.GetString("class")
level := m.form.GetInt("level")
}
return m, cmd
}
func (m Model) View() string {
if m.form.State == huh.StateCompleted {
return "Done!"
}
return m.form.View()
}
Use .Key("name") on fields to read via form.Get("name"), form.GetString(), form.GetInt(), form.GetBool(). Use .Value(&var) when you don't need the key-based API.
Form State & Errors
huh.ErrUserAborted
huh.ErrTimeout
errs := m.form.Errors()
form.SubmitCmd = mySubmitCmd
form.CancelCmd = myQuitCmd
Keymap Customization
km := huh.NewDefaultKeyMap()
km.Quit = key.NewBinding(key.WithKeys("ctrl+c", "q"))
km.Select.Filter = key.NewBinding(key.WithKeys("ctrl+f"), key.WithHelp("ctrl+f", "filter"))
form.WithKeyMap(km)
Examples Index
| Example | File | Demonstrates |
|---|
| Burger ordering | _examples/burger/main.go | Full multi-group form, Note, Select, MultiSelect, Input, Text, Confirm, Spinner |
| BubbleTea embed | _examples/bubbletea/main.go | huh.Form as tea.Model, Key-based results, side panel |
| Dynamic country | _examples/dynamic-country/main.go | OptionsFunc with async API call, TitleFunc |
| Conditional pages | _examples/conditional/main.go | OptionsFunc reacting to earlier selection |
| Accessibility | _examples/accessibility/main.go | WithAccessible(true) |
| Spinner | _examples/spinner/main.go | Standalone spinner with action |
| Group hide | _examples/hide/main.go | WithHide, WithHideFunc |
| Layout columns | _examples/layout/main.go | LayoutColumns |
| File picker | _examples/filepicker/main.go | NewFilePicker |
| Theme switcher | _examples/theme/main.go | Multiple built-in themes |
See .agentsmesh/skills/huh-v2/references/API.md for the complete API surface.