6.7k

Select

select is a trigger-only primitive — it takes no options attribute. Its content is the current value (placeholder shows while empty) and on-press opens. The options are composed as an anchored dropdown-menu of menu-items beside the trigger in a stack, rendered under an if — the model owns the open flag, and on-dismiss clears it when Escape, Tab, or a click outside closes the surface. In the open menu the selected option wears a trailing checkmark, and the row under the keyboard or pointer carries a full-row highlight — the two are independent, so arrowing away from the committed option never moves its mark. For a trigger the user types into, see combobox.

select
Select triggers rendered by the engine (light theme)
a select trigger with a value, and a disabled trigger — press to open the anchored menu

Markup

The full pattern: trigger and menu are siblings in a stack, the menu floats with anchor="below" (anchor-alignment="stretch" widens it to the trigger, the select-menu look), and every path back to closed goes through the model — picking dispatches pick_env and on-dismiss dispatches close_env_picker, both of which clear env_picker_open in update.

The keyboard rides the same composition with no extra wiring: on the focused trigger, Enter, Space, or an arrow key presses it (the model-owned open); with the menu mounted, Down/Up move into and walk the options (entering at the selected one) and Home/End jump to the edges — walking only moves the highlight, it never dispatches a pick. Enter or a click commits the highlighted option (one on-press, then the model closes); Escape, Tab, or a click outside dismisses through on-dismiss with the committed value untouched — focus returns to the trigger either way.

<stack width="240">
  <select placeholder="Choose environment" on-press="toggle_env_picker">{envName}</select>
  <if test="{env_picker_open}">
    <dropdown-menu anchor="below" anchor-alignment="stretch" on-dismiss="close_env_picker">
      <for each="environments" key="id" as="e">
        <menu-item selected="{e.id == env_id}" on-press="pick_env:{e.id}">{e.name}</menu-item>
      </for>
    </dropdown-menu>
  </if>
</stack>

The core side is the open flag, the committed choice, and three arms:

// model: { readonly env_id: number; readonly env_picker_open: boolean }
case "toggle_env_picker":
  return { ...model, env_picker_open: !model.env_picker_open };
case "close_env_picker":
  return { ...model, env_picker_open: false };
case "pick_env":
  return { ...model, env_id: msg.id, env_picker_open: false };
.toggle_env_picker => model.env_picker_open = !model.env_picker_open,
.close_env_picker => model.env_picker_open = false,
.pick_env => |id| {
    model.env_id = id;
    model.env_picker_open = false;
},

Programmatic construction (Zig)

In a Zig view, the canvas.Ui builder constructs the same tree programmatically:

ui.stack(.{ .width = 240 }, .{
    ui.el(.select, .{
        .text = model.envName(),
        .placeholder = "Choose environment",
        .on_press = .toggle_env_picker,
    }, .{}),
    if (model.env_picker_open) ui.el(.dropdown_menu, .{
        .anchor = .below,
        .anchor_alignment = .stretch,
        .on_dismiss = .close_env_picker,
    }, .{
        ui.el(.menu_item, .{ .text = "Production", .selected = model.env_id == 0, .on_press = .{ .pick_env = 0 } }, .{}),
        ui.el(.menu_item, .{ .text = "Staging", .selected = model.env_id == 1, .on_press = .{ .pick_env = 1 } }, .{}),
        ui.el(.menu_item, .{ .text = "Development", .selected = model.env_id == 2, .on_press = .{ .pick_env = 2 } }, .{}),
    }) else ui.spacer(0),
})

Attributes

AttributeDescription
textText value for text-bearing elements; a literal or one {binding}.
placeholderHint text shown while a text entry is empty.
disabledDisables the control; true/false or a {binding}.
on-pressDispatch a Msg on press: tag or tag:{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text).
on-dismissDismissible surfaces only (dialog, drawer, sheet, dropdown-menu): Msg dispatched when Escape or a click outside dismisses the surface, so the MODEL owns the close (clear the open flag in update). The engine hides the surface immediately as an optimistic echo; the source tree wins on the next rebuild.