1---
2name: charm-huh
3description: "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)."
4---
5
6# charmbracelet/huh
7
8Interactive terminal forms and prompts for Go. Built on Bubble Tea.
9
10Import: `charm.land/huh/v2`
11
12## Quick Start
13
14```go
15package main
16
17import (
18 "fmt"
19 "log"
20
21 "charm.land/huh/v2"
22)
23
24func main() {
25 var name string
26 var confirm bool
27
28 err := huh.NewForm(
29 huh.NewGroup(
30 huh.NewInput().
31 Title("What's your name?").
32 Value(&name).
33 Validate(huh.ValidateNotEmpty()),
34 huh.NewConfirm().
35 Title("Ready?").
36 Value(&confirm),
37 ),
38 ).Run()
39 if err != nil {
40 log.Fatal(err)
41 }
42 fmt.Printf("Hello, %s!\n", name)
43}
44```
45
46Single field shorthand (no Form/Group wrapper needed):
47
48```go
49var name string
50huh.NewInput().Title("Name?").Value(&name).Run()
51```
52
53## Core API
54
55### Architecture
56
57`Form` > `Group` (pages) > `Field` (inputs)
58
59Groups are displayed one at a time. The form advances to the next group when all fields in the current group pass validation.
60
61### Form
62
63```go
64form := huh.NewForm(groups...*Group) *Form
65```
66
67Key methods:
68
69| Method | Purpose |
70|--------|---------|
71| `.Run()` | Block and run the form |
72| `.RunWithContext(ctx)` | Run with context (supports cancellation) |
73| `.WithTheme(theme)` | Set theme |
74| `.WithWidth(w)` / `.WithHeight(h)` | Set dimensions |
75| `.WithAccessible(bool)` | Screen reader mode |
76| `.WithShowHelp(bool)` | Toggle help bar |
77| `.WithShowErrors(bool)` | Toggle error display |
78| `.WithTimeout(duration)` | Auto-cancel after duration |
79| `.WithLayout(layout)` | Set group layout |
80| `.WithKeyMap(keymap)` | Custom keybindings |
81| `.WithOutput(w)` / `.WithInput(r)` | Custom IO |
82
83Retrieve values by key after completion:
84
85```go
86form.GetString("key")
87form.GetInt("key")
88form.GetBool("key")
89form.Get("key") // any
90```
91
92Form states: `huh.StateNormal`, `huh.StateCompleted`, `huh.StateAborted`
93
94Errors: `huh.ErrUserAborted` (ctrl+c), `huh.ErrTimeout`
95
96### Group
97
98```go
99group := huh.NewGroup(fields ...Field) *Group
100```
101
102| Method | Purpose |
103|--------|---------|
104| `.Title(s)` / `.Description(s)` | Group header |
105| `.WithHide(bool)` | Skip this group |
106| `.WithHideFunc(func() bool)` | Conditionally skip group |
107| `.WithShowHelp(bool)` | Toggle help for group |
108
109### Field Types
110
111Every field supports: `.Title(s)`, `.Description(s)`, `.Key(s)`, `.Value(&v)`, `.Validate(fn)`, `.Run()`.
112
113Dynamic variants exist for most properties: `.TitleFunc(fn, binding)`, `.DescriptionFunc(fn, binding)`, etc.
114
115#### Input
116
117Single line text. Type: `string`.
118
119```go
120huh.NewInput().
121 Title("Email").
122 Placeholder("you@example.com").
123 Prompt("> ").
124 CharLimit(100).
125 Suggestions([]string{"gmail.com", "outlook.com"}).
126 EchoMode(huh.EchoModePassword). // or EchoModeNone
127 Inline(true). // title and input on same line
128 Validate(huh.ValidateNotEmpty()).
129 Value(&email)
130```
131
132#### Text
133
134Multi-line textarea. Type: `string`.
135
136```go
137huh.NewText().
138 Title("Description").
139 Lines(5).
140 CharLimit(500).
141 Placeholder("Enter details...").
142 ShowLineNumbers(true).
143 Editor("vim"). // external editor support (ctrl+e)
144 EditorExtension("md").
145 ExternalEditor(false). // disable external editor
146 Value(&description)
147```
148
149#### Select
150
151Pick one from a list. Generic: `Select[T comparable]`.
152
153```go
154huh.NewSelect[string]().
155 Title("Country").
156 Options(
157 huh.NewOption("United States", "US"),
158 huh.NewOption("Canada", "CA"),
159 ).
160 Height(8). // scrollable if options exceed height
161 Inline(true). // horizontal left/right navigation
162 Filtering(true). // start with filter active
163 Value(&country)
164```
165
166Shorthand for simple options:
167
168```go
169Options(huh.NewOptions("Warrior", "Mage", "Rogue")...)
170```
171
172#### MultiSelect
173
174Pick zero or more. Generic: `MultiSelect[T comparable]`.
175
176```go
177huh.NewMultiSelect[string]().
178 Title("Toppings").
179 Options(
180 huh.NewOption("Lettuce", "lettuce").Selected(true),
181 huh.NewOption("Tomato", "tomato"),
182 huh.NewOption("Cheese", "cheese"),
183 ).
184 Limit(3).
185 Height(6).
186 Filterable(false). // disable "/" filter
187 Value(&toppings)
188```
189
190Space to toggle, `a` to select all (when no limit), `/` to filter.
191
192#### Confirm
193
194Yes/No. Type: `bool`.
195
196```go
197huh.NewConfirm().
198 Title("Continue?").
199 Affirmative("Yes!").
200 Negative("No way").
201 Inline(true).
202 Value(&ok)
203```
204
205Keys: `h`/`l` or left/right to toggle, `y` to accept, `n` to reject.
206
207#### Note
208
209Display-only. Not interactive by default (auto-skipped).
210
211```go
212huh.NewNote().
213 Title("Welcome").
214 Description("This form collects your _preferences_.").
215 Height(10).
216 Next(true). // show a "Next" button, makes it interactive
217 NextLabel("Continue")
218```
219
220Description supports basic markdown: `_italic_`, `*bold*`, `` `code` ``.
221
222## Common Patterns
223
224### Multi-Step Forms
225
226Groups act as pages. Users navigate forward/back between them.
227
228```go
229huh.NewForm(
230 huh.NewGroup(/* step 1 fields */).Title("Step 1"),
231 huh.NewGroup(/* step 2 fields */).Title("Step 2"),
232 huh.NewGroup(/* step 3 fields */).Title("Step 3"),
233).Run()
234```
235
236### Conditional Groups
237
238Hide groups based on previous answers:
239
240```go
241var wantExtras bool
242
243huh.NewForm(
244 huh.NewGroup(
245 huh.NewConfirm().Title("Want extras?").Value(&wantExtras),
246 ),
247 huh.NewGroup(
248 huh.NewInput().Title("Extra details").Value(&details),
249 ).WithHideFunc(func() bool { return !wantExtras }),
250).Run()
251```
252
253### Dynamic Fields
254
255Use `*Func` variants to recompute properties when bindings change. Pass a pointer to the bound variable.
256
257```go
258var country string
259
260huh.NewSelect[string]().
261 Value(&state).
262 TitleFunc(func() string {
263 if country == "Canada" { return "Province" }
264 return "State"
265 }, &country).
266 OptionsFunc(func() []huh.Option[string] {
267 return huh.NewOptions(statesByCountry[country]...)
268 }, &country)
269```
270
271The binding (`&country`) tells huh when to recompute. Results are cached per binding hash.
272
273### Validation
274
275Built-in validators:
276
277```go
278huh.ValidateNotEmpty()
279huh.ValidateMinLength(3)
280huh.ValidateMaxLength(100)
281huh.ValidateLength(3, 100) // min and max
282huh.ValidateOneOf("a", "b", "c")
283```
284
285Custom validation:
286
287```go
288.Validate(func(s string) error {
289 if !strings.Contains(s, "@") {
290 return fmt.Errorf("must be a valid email")
291 }
292 return nil
293})
294```
295
296Validation runs on blur (when leaving a field). Forms block progression if any field in the current group has errors.
297
298### Theming
299
300Built-in themes: `ThemeCharm` (default), `ThemeDracula`, `ThemeCatppuccin`, `ThemeBase16`, `ThemeDefault`.
301
302```go
303form.WithTheme(huh.ThemeFunc(huh.ThemeDracula))
304```
305
306Custom theme - implement the `Theme` interface:
307
308```go
309type Theme interface {
310 Theme(isDark bool) *Styles
311}
312```
313
314Or use `ThemeFunc`:
315
316```go
317form.WithTheme(huh.ThemeFunc(func(isDark bool) *huh.Styles {
318 s := huh.ThemeCharm(isDark) // start from a base
319 s.Focused.Title = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212"))
320 return s
321}))
322```
323
324### Layouts
325
326```go
327form.WithLayout(huh.LayoutDefault) // one group at a time (default)
328form.WithLayout(huh.LayoutStack) // all groups stacked vertically
329form.WithLayout(huh.LayoutColumns(2)) // groups in 2 columns
330form.WithLayout(huh.LayoutGrid(2, 3)) // 2 rows, 3 columns
331```
332
333### Accessibility
334
335```go
336accessible := os.Getenv("ACCESSIBLE") != ""
337form.WithAccessible(accessible)
338```
339
340When `TERM=dumb`, accessible mode activates automatically. Replaces TUI with plain text prompts. Timeout is not supported in accessible mode.
341
342### Spinner
343
344Separate package for loading indicators after form submission:
345
346```go
347import "charm.land/huh/v2/spinner"
348
349err := spinner.New().
350 Title("Processing...").
351 Action(func() { /* do work */ }).
352 Run()
353```
354
355Or with context:
356
357```go
358go doWork()
359spinner.New().Title("Working...").Context(ctx).Run()
360```
361
362## Integration: Standalone vs Bubble Tea
363
364### Standalone
365
366Call `.Run()` on a form or individual field. Blocks until complete.
367
368```go
369form.Run()
370// or
371huh.NewInput().Title("Name?").Value(&name).Run()
372```
373
374### Embedded in Bubble Tea
375
376`*huh.Form` implements `tea.Model`. Use it as a component in your Bubble Tea app.
377
378```go
379type Model struct {
380 form *huh.Form
381}
382
383func NewModel() Model {
384 return Model{
385 form: huh.NewForm(
386 huh.NewGroup(
387 huh.NewSelect[string]().
388 Key("class").
389 Options(huh.NewOptions("Warrior", "Mage", "Rogue")...).
390 Title("Choose your class"),
391 ),
392 ),
393 }
394}
395
396func (m Model) Init() tea.Cmd {
397 return m.form.Init()
398}
399
400func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
401 form, cmd := m.form.Update(msg)
402 if f, ok := form.(*huh.Form); ok {
403 m.form = f
404 }
405
406 if m.form.State == huh.StateCompleted {
407 return m, tea.Quit
408 }
409 return m, cmd
410}
411
412func (m Model) View() string {
413 if m.form.State == huh.StateCompleted {
414 return fmt.Sprintf("You picked: %s", m.form.GetString("class"))
415 }
416 return m.form.View()
417}
418```
419
420Key differences when embedded:
421- Do NOT call `.Run()`, use Init/Update/View cycle instead
422- Set `SubmitCmd` and `CancelCmd` if you want custom behavior on form completion
423- Use `.Key("name")` on fields, retrieve with `form.GetString("name")`
424- Check `form.State` to know when the form is done
425- Type assert the Update result: `form.(*huh.Form)`
426
427Navigation methods available for programmatic control:
428- `form.NextGroup()`, `form.PrevGroup()`
429- `form.NextField()`, `form.PrevField()`
430- `form.GetFocusedField()`
431
432## Common Mistakes
433
4341. **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()`.
435
4362. **Using `.Run()` inside Bubble Tea** - Never call `.Run()` on an embedded form. Use the Init/Update/View pattern.
437
4383. **Missing type parameter on Select/MultiSelect** - `huh.NewSelect[string]()` not `huh.NewSelect()`. The generic parameter determines the option value type.
439
4404. **Dynamic binding without pointer** - `TitleFunc(fn, country)` will not work. Must be `TitleFunc(fn, &country)` with a pointer so huh can detect changes.
441
4425. **Timeout in accessible mode** - `WithTimeout()` returns `ErrTimeoutUnsupported` in accessible mode. Guard it.
443
4446. **Not handling ErrUserAborted** - `form.Run()` returns `huh.ErrUserAborted` when user presses ctrl+c. Always check the error.
445
4467. **Form outputs to stderr** - By default, the TUI renders to stderr (stdout stays clean for piping). Use `.WithOutput(os.Stdout)` to change.
447
448## Checklist
449
450- [ ] Import `charm.land/huh/v2`
451- [ ] Every field that stores data has `.Value(&var)` or `.Key("name")`
452- [ ] Custom validators return `nil` on success, `error` on failure
453- [ ] Dynamic fields use `*Func` variants with pointer bindings
454- [ ] Accessible mode handled via env var or config flag
455- [ ] `ErrUserAborted` handled after `.Run()`
456- [ ] Embedded forms use Init/Update/View, not `.Run()`
457- [ ] Form state checked via `form.State == huh.StateCompleted`