Skip to content

07 - Forms and controlled state

Form controls display a value from the model and report user intent through a callback. The model remains the source of truth.

Minimal form

type Model struct {
    Name string
}

type NameChanged struct{ Value string }

func View(_ *ui.Context, m Model, send ui.Send[Msg]) ui.Widget {
    return layout.Column(
        input.New(m.Name).
            Hint("Name").
            OnChange(func(value string) {
                send(NameChanged{Value: value})
            }),
        text.New("Hello, "+m.Name),
    ).Gap(12)
}

Run go run ./examples/inputs from the repository root for controlled inputs. Other examples in this chapter are usage snippets.

Common fields

input.New(m.Email).
    Hint("you@example.com").
    OnChange(func(value string) { send(EmailChanged{Value: value}) })

input.NewTextArea(m.Bio).
    OnChange(func(value string) { send(BioChanged{Value: value}) })

toggle.NewCheckbox("I agree to the terms").Key("agree").Checked(m.Agree).
    OnChange(func(value bool) { send(AgreeChanged{Value: value}) })

Right-click edit menu

Input and TextArea ship with a built-in right-click edit menu (Cut / Copy / Paste / Select All). Entries enable and disable themselves based on the read-only, password, and selection state: password fields never offer cut or copy, and read-only fields never offer cut or paste. Shortcut hints follow the platform (Ctrl on Windows / Linux, Command on macOS), and the menu labels follow the interface language (Chinese / English).

Fields inside input.Group / GroupTextArea and the combobox.New input share this built-in menu, with the same .DefaultContextMenu(false) and .ContextMenu(...) options; text.Selectable ships a right-click menu with Copy / Select All. While a menu is open, the selection highlight stays visible instead of disappearing when the menu takes focus.

Call .DefaultContextMenu(false) to turn the built-in menu off, or .ContextMenu(...) to replace it entirely with a custom menu:

input.New(m.Email).DefaultContextMenu(false)

input.New(m.Email).
    ContextMenu(menu.New("email-menu", []menu.Item{
        {Key: "copy", Label: "Copy address"},
    }))

input.Group actions

Use Prefix and Suffix for static content. Use PrefixAction and SuffixAction for clickable affixes. input.GroupAction provides a stable 24dp target, pointer cursor, and accessible label:

input.Group(input.New(m.Website)).
    SuffixAction(
        input.GroupAction("clear-website", "Clear website").Content(icon.New(lucide.X).Size(16)).
            OnClick(func() { send(ClearWebsite{}) }),
    )

Action slots do not focus the editor by default. Call .FocusOnActionPress(true) when the editor should regain focus after an action. Override the default slot spacing with PrefixPadding or SuffixPadding when a custom layout needs it. The editor face is PartContent; use style.New().Part(style.PartContent, ...) to customize its cursor, text, or typography without changing the outer group shell.

Selection controls use model keys:

selector.New(items).Key("state").SelectedKey(m.State).
    Label("State").
    Placeholder("Select one").
    OnChange(func(key string) { send(SetState(key)) })

Use Invalid and ErrorMessage for validation feedback. Validate and update the error fields in Update, usually when the user submits.

Open state

Select, dropdown, context-menu, popover, and modal controls support two modes:

No Open(...)       -> uncontrolled; the widget owns open state
Open(bool)         -> controlled; the model owns open state
DefaultOpen(bool)  -> initial value for uncontrolled state

Controlled usage must update the model from the component's open-state callback:

selector.New(items).Key("city").SelectedKey(m.City).
    Open(m.CityOpen).
    OnOpenChange(func(open bool) { send(SetCityOpen(open)) }).
    OnChange(func(key string) { send(SetCity(key)) })

Calling Open(...) without handling the callback leaves the panel stuck at the value supplied by the model.

Dropdown uses OnOpenChangeEvent and reads the new state from event.Open. Select, ContextMenu, Popover, and Modal continue to use OnOpenChange.

Labels and descriptions

Prefer a control's Label and Description methods. For custom compositions, associate a Label with the field key so keyboard focus and accessibility semantics remain correct.

Continue with Commands and subscriptions.