Skip to main content

Option groups, separators and disabled options

Every field that draws a list - select, search, suggest, toggle and reorder, and the ->multiple() variants of the first two - accepts more than a flat one. Alongside the ->options(['value' => 'Label']) map shorthand, you can declare options one at a time, mark them disabled, and add non-selectable structure. Declaring any of them on a kind that shows no list throws where it is written.

$p->select('item', 'Item')
->heading('Fruit') // A non-selectable group heading.
->option('apple', 'Apple') // value => label
->option('banana', 'Banana')
->separator() // A non-selectable divider row.
->heading('Vegetable')
->option('carrot', 'Carrot')
->option('cherry', 'Cherry', disabled: TRUE, disabled_reason: 'out of season');

Runnable scripts: playground/02-fields-select-groups.php and select-multiple-groups.php.

Builder methods

NameDescription
option($value, $label = '', ...)Add one selectable row. The label defaults to the value; re-declaring a value replaces it in place. Pass disabled: TRUE (with an optional disabled_reason) to show it but block selection, or description: for a contextual line shown when it is highlighted.
options([$value => $label])Add many selectable rows from a map - shorthand for repeated option().
heading($label)Add a non-selectable group-heading row.
separator()Add a non-selectable divider row.

Behavior

On a select and a search, headings, separators and disabled options are visual only: navigation skips them, so the cursor lands only on selectable options, and they can never be highlighted or selected. A disabled option shows its reason beside the label, dimmed.

The other three kinds draw no such structure, each for its own reason. A suggest matches typed text against its selectable values rather than drawing a list to walk, so a heading or a separator is accepted where it is written and simply never reaches the field. A toggle is exactly two values and a reorder is a ranking of plain ones, so either refuses a structural row when the form is built, naming the field. Every kind is theme-driven: a heading and a disabled label are drawn by fieldEntry(), a divider by fieldEntrySeparator() and a disabled reason by fieldEntryNote(), so overriding one of those elements restyles it.

Non-selectable rows never leak into the answer: a disabled value is dropped from a multiple-choice default, absent from the collected value, and excluded from the JSON schema (Tui::schema() lists selectable options only). Supplying a disabled - or otherwise unknown - option value headlessly (via --prompts JSON or an environment override) fails collection with a clear error naming the value.

Examples

A single-choice select with a group heading, a separator and a disabled option (its reason shown beside the dimmed label):

ANSINo ANSI
UnicodeSelect with groups: Unicode + ANSISelect with groups: Unicode + ANSISelect with groups: Unicode + No ANSISelect with groups: Unicode + No ANSI
ASCIISelect with groups: ASCII + ANSISelect with groups: ASCII + ANSISelect with groups: ASCII + No ANSISelect with groups: ASCII + No ANSI

A multiple select where the cursor and Space skip the separator and the disabled option, which can never be checked:

ANSINo ANSI
UnicodeSelect (multiple) with groups: Unicode + ANSISelect (multiple) with groups: Unicode + ANSISelect (multiple) with groups: Unicode + No ANSISelect (multiple) with groups: Unicode + No ANSI
ASCIISelect (multiple) with groups: ASCII + ANSISelect (multiple) with groups: ASCII + ANSISelect (multiple) with groups: ASCII + No ANSISelect (multiple) with groups: ASCII + No ANSI