7.5k

Radio

The single-choice value control, grouped by a radio-group. Give the group an accessible label that names the shared choice. Like checkbox, each radio's label rides the text attribute — radio is not a text-bearing element, so text content between the tags is rejected with a teaching error. Descendant radios at any nesting depth form one logical group: one Tab stop, arrows wrap through the choices while Home/End move to the edges, focus and selection move together, and selecting one clears the rest. Bind the model's choice through checked; an actual selection transition dispatches on-change when bound, then falls back to on-toggle and on-press for compatibility. Activating the already-checked radio has no new on-change edge (a legacy fallback handler still receives the activation).

radio-group
A radio group rendered by the engine (light theme)
a radio group with one selected and one disabled option

Markup

<radio-group gap="12" label="Density">
  <radio checked="{density == default}" on-change="set_default" text="Default" />
  <row>
    <radio checked="{density == comfortable}" on-change="set_comfortable" text="Comfortable" />
  </row>
  <radio checked="{density == compact}" disabled="true" text="Compact" />
</radio-group>

One field holds the group's choice — a string-literal union in a TypeScript core, an enum in a Zig core — and each radio's arm sets it:

// model: { readonly density: "default" | "comfortable" | "compact" }
case "set_comfortable":
  return { ...model, density: "comfortable" };
// model: density: enum { default, comfortable, compact } = .default,
.set_comfortable => model.density = .comfortable,

Programmatic construction (Zig)

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

ui.el(.radio_group, .{ .gap = 12, .semantics = .{ .label = "Density" } }, .{
    ui.el(.radio, .{ .text = "Default", .checked = model.density == .default, .on_change = .set_default }, .{}),
    ui.row(.{}, .{
        ui.el(.radio, .{ .text = "Comfortable", .checked = model.density == .comfortable, .on_change = .set_comfortable }, .{}),
    }),
    ui.el(.radio, .{ .text = "Compact", .checked = model.density == .compact, .disabled = true }, .{}),
})

Attributes

AttributeDescription
textText value for text-bearing elements; a literal or one {binding}.
checkedChecked state for checkbox/toggle; true/false or a {binding}.
disabledDisables the control; true/false or a {binding}.
on-changeDispatch a Msg on change: tag or tag:{payload}. Hit-target elements only. On a treeitem, Up/Down/Left/Right/Home/End focus movement prefers on-change over on-press, separating keyboard selection from pointer activation.
on-toggleDispatch a Msg on toggle: tag or tag:{payload}. Hit-target elements only (checkbox, toggle, toggle-button, switch, accordion, ...).
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).