Inputs

July 9, 2026 · View on GitHub

Text fields, checkboxes, toggles, radio buttons, sliders, number steppers, dropdowns, and a search field with a clear button.


Input — Single-line Text Field

type UI struct {
    email proton.Editor
}

proton.Input(ctx, &u.email, "your@email.com")

// read the value any time
fmt.Println(u.email.Text())

The second argument is the placeholder text shown when the field is empty.

proton.Input(ctx proton.Context, state *proton.Editor, hint string)

TextArea — Multi-line Text Field

Same as Input but the user can press Enter to add lines. Good for messages, notes, anything longer than a single line.

type UI struct {
    bio proton.Editor
}

proton.TextArea(ctx, &u.bio, "Tell us something...")

fmt.Println(u.bio.Text())
proton.TextArea(ctx proton.Context, state *proton.Editor, hint string)

SearchInput

A text field with a search icon on the left and a clear (×) button that appears when there's something to clear. Returns the current query string.

type UI struct {
    search proton.SearchState
}

q := proton.SearchInput(ctx, &u.search, "Search notes...")

// filter your data using q
filtered := filter(items, q)

SearchState holds both the Editor and the internal clear button — declare one in your struct, don't construct it yourself.

proton.SearchInput(ctx proton.Context, state *proton.SearchState, placeholder string) string

Checkbox

Returns true on the frame the user toggles it. Read the current value from state.Value.

type UI struct {
    agreed proton.Bool
}

if proton.Checkbox(ctx, &u.agreed, "I have read the terms and conditions") {
    // just changed — u.agreed.Value is the new state
    fmt.Println("now:", u.agreed.Value)
}

// read it any time without caring about the change event
if u.agreed.Value {
    proton.SuccessText(ctx, "Thanks for agreeing (we know you didn't read it)")
}
proton.Checkbox(ctx proton.Context, state *proton.Bool, label string) bool

Toggle

A material-style on/off switch. Same API as Checkbox, different look. Use for settings that take effect immediately rather than needing a Save button.

type UI struct {
    darkMode proton.Bool
}

if proton.Toggle(ctx, &u.darkMode, "Dark mode") {
    if u.darkMode.Value {
        applyDarkTheme()
    } else {
        applyLightTheme()
    }
}
proton.Toggle(ctx proton.Context, state *proton.Bool, label string) bool

RadioButton

For picking exactly one option from a group. All buttons in a group share one proton.Enum state field. The key is what gets stored in group.Value when that option is selected.

type UI struct {
    plan proton.Enum
}

proton.RadioButton(ctx, &u.plan, "free", "Free")
proton.Gap(ctx, 4)
proton.RadioButton(ctx, &u.plan, "pro", "Pro — \$9/mo")
proton.Gap(ctx, 4)
proton.RadioButton(ctx, &u.plan, "team", "Team — \$29/mo")

fmt.Println("selected:", u.plan.Value) // "free", "pro", or "team"

Returns true on the frame the selection changes.

proton.RadioButton(ctx proton.Context, group *proton.Enum, key string, label string) bool

Horizontal radio buttons — wrap them in Row:

proton.Row(ctx,
    func(ctx proton.Context) { proton.RadioButton(ctx, &u.size, "s", "S") },
    func(ctx proton.Context) { proton.Gap(ctx, 12) },
    func(ctx proton.Context) { proton.RadioButton(ctx, &u.size, "m", "M") },
    func(ctx proton.Context) { proton.Gap(ctx, 12) },
    func(ctx proton.Context) { proton.RadioButton(ctx, &u.size, "l", "L") },
)

Slider

A horizontal drag handle for a value between 0.0 and 1.0. Scale it to whatever range you need.

type UI struct {
    vol proton.Float
}

v := proton.Slider(ctx, &u.vol)

// v is 0.0–1.0, scale it
volume := int(v * 100)
proton.Caption(ctx, fmt.Sprintf("Volume: %d%%", volume))

You can also read the value directly from the state:

proton.Slider(ctx, &u.vol)
fmt.Println(u.vol.Value) // 0.0 to 1.0
proton.Slider(ctx proton.Context, state *proton.Float) float32

ProgressBar

Not interactive — just shows progress as a filled bar. Pass a float32 between 0.0 and 1.0.

proton.ProgressBar(ctx, 0.65)    // 65% done
proton.ProgressBar(ctx, 1.0)     // done
proton.ProgressBar(ctx, progress) // from a variable
proton.ProgressBar(ctx proton.Context, progress float32)

NumberInput

A stepper with − and + buttons. Handles min, max, and step size for you. Returns the current value.

type UI struct {
    qty    proton.NumberState
    rating proton.NumberState
}

// integers
qty := proton.NumberInput(ctx, &u.qty, 1, 99, 1)
proton.Caption(ctx, fmt.Sprintf("%d items", int(qty)))

// floats
rating := proton.NumberInput(ctx, &u.rating, 0, 5, 0.5)
proton.Caption(ctx, fmt.Sprintf("%.1f / 5.0", rating))
proton.NumberInput(ctx proton.Context, state *proton.NumberState, min, max, step float64) float64

The value starts at min on first use. Step >= 1 displays integers; step < 1 displays two decimal places.


SelectBox

A dropdown selector. Returns the index of the currently selected option.

type UI struct {
    lang proton.SelectBoxState
}

langs := []string{"Go", "Rust", "Zig", "C", "Python"}

i := proton.SelectBox(ctx, &u.lang, langs)
proton.Caption(ctx, "You picked: "+langs[i])

The dropdown appears below the button when clicked. Clicking anywhere outside it closes it.

proton.SelectBox(ctx proton.Context, state *proton.SelectBoxState, options []string) int

Selected starts at 0. Check state.Selected >= 0 if you need to know whether the user has explicitly picked something.


Full Form Example

type SettingsUI struct {
    username proton.Editor
    bio      proton.Editor
    notify   proton.Bool
    dark     proton.Bool
    plan     proton.Enum
    volume   proton.Float
    save     proton.Clickable
}

func drawSettings(ctx proton.Context, s *SettingsUI) {
    proton.H4(ctx, "Settings")
    proton.Gap(ctx, 20)

    proton.Label(ctx, "Username")
    proton.Gap(ctx, 4)
    proton.Input(ctx, &s.username, "your_username")
    proton.Gap(ctx, 14)

    proton.Label(ctx, "Bio")
    proton.Gap(ctx, 4)
    proton.TextArea(ctx, &s.bio, "Tell us something...")
    proton.Gap(ctx, 20)

    proton.Toggle(ctx, &s.dark, "Dark mode")
    proton.Gap(ctx, 8)
    proton.Checkbox(ctx, &s.notify, "Email notifications")
    proton.Gap(ctx, 20)

    proton.Label(ctx, "Plan")
    proton.Gap(ctx, 6)
    proton.RadioButton(ctx, &s.plan, "free", "Free")
    proton.Gap(ctx, 4)
    proton.RadioButton(ctx, &s.plan, "pro", "Pro (\$9/mo)")
    proton.Gap(ctx, 4)
    proton.RadioButton(ctx, &s.plan, "team", "Team (\$29/mo)")
    proton.Gap(ctx, 20)

    proton.Label(ctx, fmt.Sprintf("Volume: %.0f%%", s.volume.Value*100))
    proton.Gap(ctx, 4)
    proton.Slider(ctx, &s.volume)
    proton.Gap(ctx, 28)

    proton.Pad(ctx, 4, func(ctx proton.Context) {
        if proton.Button(ctx, &s.save, "Save Settings") {
            handleSave(s)
        }
    })
}