Radio Group
Select exactly one value from a set.
use herogpui::components::radio_group::RadioGroup;RadioGroup on docs.rsSource at v0.14.0
Usage
Showing Usage, live: HeroGPUI compiled to WebAssembly. The canvas has no accessibility tree; each example's description and code below carry the same information.
Option content uses 14px medium text with 20px lines, independent of surrounding line height. An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::{RadioGroup, RadioOption};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let plans =
|| -> Vec<RadioOption> { vec!["Free".into(), "Pro".into(), "Enterprise".into()] };
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![RadioGroup::new("radio-group", plans())
.full_width(true)
.default_value("Free")
// v3's own example opens with the group's `<Label>` and
// `<Description>`, then a `<Description>` per `<Radio>`.
.label("Plan selection")
.description("Choose the plan that suits you best")
.descriptions([
Some("Includes 100 messages per month"),
Some("Includes 200 messages per month"),
None,
])
.into_any_element()])`text_size` replaces the option labels' `text-sm`; a 12/14/16px size takes v3's 16/20/24 leading. The circles, gap and descriptions keep their size step. An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::{RadioGroup, RadioOption};
use herogpui::gpui::px;
let plans =
|| -> Vec<RadioOption> { vec!["Free".into(), "Pro".into(), "Enterprise".into()] };
RadioGroup::new("radio-group-label-size", plans())
.text_size(px(16.))
.default_value("Free")`size` is additive, not a prop: `Md` is the 16px control; `Sm` is a 14px control with a 5px dot, 12px label text and a 10px row gap. An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::{RadioGroup, RadioOption, RadioSize};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let plans =
|| -> Vec<RadioOption> { vec!["Free".into(), "Pro".into(), "Enterprise".into()] };
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
RadioGroup::new("radio-group-sz-sm", plans())
.size(RadioSize::Sm)
.descriptions([Some("100 messages"), Some("200 messages"), None])
.default_value("Free")
.into_any_element(),
RadioGroup::new("radio-group-sz-md", plans())
.size(RadioSize::Md)
.descriptions([Some("100 messages"), Some("200 messages"), None])
.default_value("Free")
.into_any_element(),
])`radius(px)` rounds the control circle in place of `key_radius`; the pressed box scales the same value and the selected dot keeps its own. An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::{FieldVariant, RadioGroup, RadioOption};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let plans =
|| -> Vec<RadioOption> { vec!["Free".into(), "Pro".into(), "Enterprise".into()] };
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
RadioGroup::new("radio-group-primary", plans())
.default_value("Free")
.radius(px(4.))
.into_any_element(),
RadioGroup::new("radio-group-secondary", plans())
.default_value("Pro")
.variant(FieldVariant::Secondary)
.into_any_element(),
])An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::{FieldVariant, RadioGroup, RadioOption, Surface};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let plans =
|| -> Vec<RadioOption> { vec!["Free".into(), "Pro".into(), "Enterprise".into()] };
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![Surface::new()
.padding(px(24.))
.child(
RadioGroup::new("radio-group-surface", plans())
.default_value("Free")
.variant(FieldVariant::Secondary),
)
.into_any_element()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{RadioGroup, RadioOption};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let plans =
|| -> Vec<RadioOption> { vec!["Free".into(), "Pro".into(), "Enterprise".into()] };
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
RadioGroup::new("radio-group-validate", plans())
.default_value("")
.label("Plan")
.is_required(true)
// v3 composes a `<FieldError>` in the group;
// supplying it is what marks the group invalid.
.error_message("Choose a plan to continue")
.into_any_element(),
RadioGroup::new(
"radio-group-option-error",
vec![
RadioOption::new("Standard delivery"),
RadioOption::new("Express delivery")
.error_message("Unavailable for this address"),
],
)
.label("Delivery speed")
.into_any_element(),
])An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::{Orientation, RadioGroup, Separator};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
RadioGroup::new(
"radio-group-delivery",
vec![
"Standard — 5 to 7 days".into(),
"Express — 2 days".into(),
"Overnight".into(),
],
)
.default_value("Standard — 5 to 7 days")
.into_any_element(),
Separator::new().into_any_element(),
RadioGroup::new(
"radio-group-payment",
vec!["Card".into(), "Bank transfer".into(), "Invoice".into()],
)
.default_value("Card")
.orientation(Orientation::Horizontal)
.into_any_element(),
])The checkmark replaces `Radio.Indicator` while the control, selection and focus behavior stay owned by the radio. An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::{ActiveTheme, Color, RadioGroup, RadioOption, icons};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let indicator_color = cx.role(Color::Accent).foreground;
let plans =
|| -> Vec<RadioOption> { vec!["Free".into(), "Pro".into(), "Enterprise".into()] };
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
RadioGroup::new("radio-group-indicator", plans())
.default_value("Enterprise")
.indicator(move |_, state| {
if state.is_selected {
gpui::svg()
.size(px(12.))
.path(icons::CHECK)
.text_color(indicator_color)
.into_any_element()
} else {
gpui::div().into_any_element()
}
})
.into_any_element(),
])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{RadioGroup, RadioOption};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;
let selected = self.radio_sel;
let options: Vec<RadioOption> = vec!["Free".into(), "Pro".into(), "Enterprise".into()];
let selected_value = SharedString::from(match selected {
Some(0) => "Free",
Some(1) => "Pro",
Some(2) => "Enterprise",
_ => "",
});
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![RadioGroup::new("radio-group", options.clone())
.value(selected_value.clone())
.on_change(cx.listener(|this, value: &SharedString, _, cx| {
this.radio_sel = match value.as_ref() {
"Free" => Some(0),
"Pro" => Some(1),
"Enterprise" => Some(2),
_ => None,
};
cx.notify();
}))
.into_any_element()])An element expression with window: &mut Window and cx: &mut App in scope.
use herogpui::prelude::{RadioGroup, RadioOption};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
let options: Vec<RadioOption> = vec!["Free".into(), "Pro".into(), "Enterprise".into()];
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![RadioGroup::new("radio-group-unc", options.clone())
.default_value("Pro")
.into_any_element()])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Orientation, RadioGroup, RadioOption};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;
let selected = self.radio_sel;
let options: Vec<RadioOption> = vec!["Free".into(), "Pro".into(), "Enterprise".into()];
let selected_value = SharedString::from(match selected {
Some(0) => "Free",
Some(1) => "Pro",
Some(2) => "Enterprise",
_ => "",
});
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![RadioGroup::new("radio-group-h", options.clone())
.value(selected_value.clone())
.orientation(Orientation::Horizontal)
.on_change(cx.listener(|this, value: &SharedString, _, cx| {
this.radio_sel = match value.as_ref() {
"Free" => Some(0),
"Pro" => Some(1),
"Enterprise" => Some(2),
_ => None,
};
cx.notify();
}))
.into_any_element()])The selected plan lives in the caller: the group reads `value` and the caption prints the selected plan. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{ActiveTheme, RadioGroup, RadioOption};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, SharedString, px};
use herogpui::gpui;
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
RadioGroup::new(
"radio-group-controlled",
vec![
RadioOption::new("Starter"),
RadioOption::new("Pro"),
RadioOption::new("Teams"),
],
)
.label("Subscription plan")
.value(self.radio_plan.clone())
.on_change(cx.listener(|this, value: &SharedString, _, cx| {
this.radio_plan = value.clone();
cx.notify();
}))
.into_any_element(),
para(
&format!("Selected plan: {}", self.radio_plan),
cx,
),
])Also calls para (included when you copy).
Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{RadioGroup, RadioOption};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;
let selected = self.radio_sel;
let options: Vec<RadioOption> = vec!["Free".into(), "Pro".into(), "Enterprise".into()];
let selected_value = SharedString::from(match selected {
Some(0) => "Free",
Some(1) => "Pro",
Some(2) => "Enterprise",
_ => "",
});
gpui::div()
.flex()
.flex_col()
.items_start()
.justify_center()
.gap(px(12.))
.children(vec![
// v3's own example disables the whole group
// (`<RadioGroup isDisabled>`); `Radio.isDisabled`
// instead disables one option — dimmed, unclickable,
// skipped by the arrows.
RadioGroup::new("radio-group-d", options)
.value(selected_value)
.is_disabled(true)
.into_any_element(),
RadioGroup::new(
"radio-group-d-opt",
vec![
RadioOption::new("Free").is_disabled(true),
"Pro".into(),
"Enterprise".into(),
],
)
.default_value("Enterprise")
.into_any_element(),
])Helpers used by these examples
Plain functions over the public API, defined once here and copied with each example that calls them.
fn para(text: &str, cx: &App) -> gpui::AnyElement {
let colors = cx.colors();
gpui::div()
.text_size(px(14.5))
.line_height(px(24.))
.text_color(colors.foreground)
.max_w(px(720.))
.child(text.to_owned())
.into_any_element()
}Anatomy
Rust types
Radio Group is assembled from these builders. The API reference lists each one.
Customization
Shared appearance belongs on ThemeBuilder::components and .recipe("name"). Typed builders change how Radio Group looks at one call site. Every builder also carries sx for placement, width and flex: it takes GPUI's own styling methods and refines them over the component's root after the theme, so an instance override wins. Behaviour builders live in the API reference below.
Styling
| Method | Values | Description |
|---|---|---|
variant(FieldVariant) | FieldVariant::Primary | FieldVariant::Secondary | Primary field chrome or the flat surface treatment. |
orientation(Orientation) | Orientation::Horizontal | Orientation::Vertical | Layout and arrow-key orientation. |
// The one slot for caller-owned low-level styling: GPUI's
// styling methods, refined over the root element last.
.sx(|el| {
el.bg(gpui::rgba(0xffa500ff)) // background
.text_color(gpui::rgba(0x000000ff)) // text
.w(gpui::px(13.)) // size
.h(gpui::px(12.))
})API reference
Against HeroUI v3.2.6: 37 entries ported, 5 partial, 1 not ported. Partial rows are marked in the tables; entries not ported are listed under each table with the reason.
Builders
| Builder | Values | Default | Description |
|---|---|---|---|
value( | string | — | Current selected value (controlled). |
default_value( | string | — | Initial selected value (uncontrolled). |
on_change( | Fn( | — | Handler called with the selected option value. |
is_disabled( | true | | false | Disables every option and removes the group from the tab order. |
is_required( | true | | false | Requires one selected value for form submission. |
is_read_only( | true | | false | Keeps roving focus active while preventing value changes. |
is_invalid( | true | | false | Marks the group and each radio control invalid. |
variant( | FieldVariant:: | FieldVariant:: | Primary field chrome or the flat surface treatment. |
name( | string | — | Shared submitted name for the selected radio value. |
orientation( | Orientation:: | Orientation:: | Layout and arrow-key orientation. |
new(Partial | AnyElement | | — | Compound label, description, radios and field error; root render props are not exposed as one closure. |
option_content(Partial | AnyElement | | — | Static option labels and state-driven content are supported, but arbitrary part ordering is not. |
indicator( | AnyElement | — | Custom content inside the control wrapper. |
option_content( | true | | — | Current option selection supplied to a custom renderer. |
Not ported1 HeroUI entry not ported
| HeroUI v3 | Why it is not ported |
|---|---|
RadioGroup.render | Overrides the group element. |
Parts
| Part | Description |
|---|---|
RadioGroup | Single-selection, form, validation and roving-focus owner. |
RadioGroup | Optional group label with required and invalid state. |
RadioGroup | Group-level or per-option help text. |
RadioOption | One option field with its own value and disabled state. |
RadioGroupPartial | Clickable row containing the control and label; arbitrary child ordering is not exposed. |
RadioGroup | Focusable 16px selection control. |
RadioGroup | Built-in selected dot or caller-rendered indicator. |
RadioGroup | Group-level or per-radio validation text outside clickable content. |
States
| State | Builder | Description |
|---|---|---|
Selected | selected == Some(index) | Accent control and foreground indicator show the chosen option. |
Hovered | interaction hovered | Changes the selected or variant-specific indicator fill on hover and adds the field-border hover layer; selected rest state keeps its accent edge transparent. |
Focus visible | tab_stop_handle | The roving keyboard stop rings the current control. |
Pressed | interaction is_pressed | The control shrinks to 95%, the selected fill hovers and the dot grows from 6px to 8px. |
Disabled group | RadioGroup::is_disabled | Every option is dimmed, inert and absent from the tab order. |
Disabled option | RadioOption::is_disabled | One option is dimmed and skipped by pointer and arrow navigation. |
Read only | is_read_only gates selection after cursor movement | Arrows move focus while selection remains unchanged. |
Invalid | self.is_invalid || group error || option error | Group invalid state outlines every control; a per-radio error outlines only its own control. |
Vertical | Orientation::Vertical | Options stack and all four radio arrows rove through them. |
Horizontal | Orientation::Horizontal | Options wrap in a row while radio-group arrows retain four-arrow contract. |