Select
Trigger + chevron, a floating panel of options, check-marked selection.
A bordered trigger showing the current choice; clicking it opens a floating
panel over the content beneath — built on the shared portal primitive (Floating),
so it escapes any parent clipping, paints above everything, and wins the pointer. Picking an
option, pressing anywhere else, or Escape closes it; the panel opens aligned to the current
selection. Web-specific composition (SelectTrigger/SelectValue
sub-components) and RTL are "from the web": Go structs replace composition, and RTL layout is
not yet supported by the underlying toolkit.
Installation
Use the module import (the default), or own the source: lotusui add
vendors the component into your app and lotusui update keeps the copy mergeable —
see Registry.
go get github.com/ikaito-com/lotusui
# or vendor the source into your app:
go run github.com/ikaito-com/lotusui/cmd/lotusui add select
Usage
Options carry a Label (what the user reads) and a
Value (what your app stores) — SelectItem / SelectOpts build lists. Call
Clear() for the placeholder state; read the choice with Value(),
never an index. Open the demo's panel: it floats over whatever is beneath.
fruit := lotusui.Select{
Options: lotusui.SelectOpts("Apple", "Banana", "Blueberry", "Grapes", "Pineapple"),
Placeholder: "Select a fruit",
}
fruit.Clear() // start on the placeholder
fruit.Layout(th, gtx, "Fruit")
chosen := fruit.Value() // "Banana"
Align item with trigger
AlignItemWithTrigger overlays the open panel so the SELECTED row
sits directly over the trigger — the native-select feel — instead of dropping below. Long
lists still scroll; the alignment accounts for scrolled-away rows.
font := lotusui.Select{Options: fonts, AlignItemWithTrigger: true}
font.SetValue("Roboto")
Groups
Groups renders options under muted labels with separators between
groups. Value() reads the choice whichever group it came from — grouping is
presentation, not identity.
produce := lotusui.Select{
Groups: lotusui.SelectGroups(
lotusui.SelectGrouped("Fruits", lotusui.SelectOpts("Apple", "Banana", "Cherry")...),
lotusui.SelectGrouped("Vegetables", lotusui.SelectOpts("Carrot", "Leek", "Spinach")...),
),
Placeholder: "Pick a produce…",
}
produce.Clear()
Scrollable
The panel caps at seven default rows; longer lists scroll inside it, and the panel
opens scrolled to the current selection — the align-with-trigger behavior, transposed.
Variable-height Content rows share the same height budget.
tz := lotusui.Select{Options: timezones}
tz.SetValue("Europe/Paris") // the panel opens scrolled to it
Meta
SelectOption.Meta is optional secondary text on the far right of
an option row (and on the closed trigger) — a count, shortcut, … Empty omits it. The selected
check still sits after Meta.
lotusui.Select{Options: []lotusui.SelectOption{
{Label: "roteland", Value: "r", Meta: "1"},
{Label: "test", Value: "t", Meta: "2"},
}}
Icons
SelectOption.Icon paints a leading icon on the row and closed trigger (when Content is nil).
lotusui.SelectItems(
{Label: "Line", Value: "line", Icon: lotusui.IconEdit},
{Label: "Bar", Value: "bar", Icon: lotusui.IconSettings},
)
Subscription plan
Build-time Content replaces Icon+Label with arbitrary widgets — the shadcn
plan card (title + description). The same Content paints in the closed trigger. Keep
Label/Value for the choice contract; build Content when the options
list is built, not every frame in hot paths.
planRow := func(name, desc string) layout.Widget {
return lotusui.VStack(2,
lotusui.LabelBody(th, name).Layout,
lotusui.LabelCaption(th, desc).Layout,
)
}
sel.Options = lotusui.SelectItems(
{Label: "Starter", Value: "starter", Content: planRow("Starter", "Perfect for individuals getting started.")},
{Label: "Professional", Value: "pro", Content: planRow("Professional", "Ideal for growing teams and businesses.")},
)
Disabled
Freezes the control on its current choice — identity fields that cannot change after creation.
Invalid
Danger chrome on the trigger — pair it with a Field error message.
Sizes
All seven shared Size presets on the trigger frame.
API
| Option | Type | Description |
|---|---|---|
Options | []SelectOption | The choices: Label is what the user reads, Value what your app stores (HTML's <option value>). Empty Value = the Label is the value; SelectItem / SelectOpts build lists. |
SelectOption.Icon | string | Leading icon on the option row and closed trigger when Content is nil. |
SelectOption.Content | layout.Widget | Build-time rich row body (multiline plan cards, …). Replaces Icon+Label in the panel and trigger; Label/Value still own identity. |
SelectOption.Meta | string | Optional secondary text on the far right of the option row and closed trigger. Empty omits it; the selected check still sits after Meta. Hidden when Content is set. |
SelectItem / SelectItemValue / SelectItems | ctors | Build-time composition: one option, valued option, pack into a slice. |
SelectGrouped / SelectGroups | ctors | Build-time groups (the type remains SelectGroup — Go cannot also export func SelectGroup). |
Groups | []SelectGroup | Wins over Options: options under muted labels, separators between groups, flattened in order. |
Size | Size | The shared size presets for the trigger frame. |
Value() / SetValue(v) | string | Read and write the CHOICE — never an index, so reordering or rewording the options cannot change what stored data means. An unknown SetValue clears the choice. |
Clear() / Chosen() | — | Back to the placeholder state; whether anything is chosen. The zero value selects the first option, like a <select> with no selected attribute. |
Placeholder | string | Shown in muted ink while nothing is chosen. |
AlignItemWithTrigger | bool | Open the panel with the selected row over the trigger (uniform string rows; Content rows drop below). |
Invalid | bool | Danger chrome on the trigger. |
Disabled | bool | Freezes the control: no pointer cursor, no opening, dimmed value. |