Skip to content
PorabuildHeroGPUI component library
v0.14.0 · Apache-2.0

Forms

Radio Group

Select exactly one value from a set.

Rust
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.

Usage

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.

Rust
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()])

Label Size

`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.

Rust
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")

Sizes

`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.

Rust
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(),
    ])

Variants

`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.

Rust
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(),
    ])

In Surface

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
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()])

Validation

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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(),
    ])

Delivery & Payment

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
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(),
    ])

Custom Indicator

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.

Rust
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(),
    ])

Vertical

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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()])

Uncontrolled

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
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()])

Horizontal Orientation

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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()])

Controlled

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>.

Rust
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).

Disabled

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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.

para
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

RadioGroupRadioOption

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

Radio Group styling
MethodValuesDescription
variant(FieldVariant)FieldVariant::Primary | FieldVariant::SecondaryPrimary field chrome or the flat surface treatment.
orientation(Orientation)Orientation::Horizontal | Orientation::VerticalLayout and arrow-key orientation.
Rust
// 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

Radio Group builders
BuilderValuesDefaultDescription
value(value)string—Current selected value (controlled).
default_value(value)string—Initial selected value (uncontrolled).
on_change(callback)Fn(value: string) -> ()—Handler called with the selected option value.
is_disabled(bool)true | falsefalseDisables every option and removes the group from the tab order.
is_required(bool)true | falsefalseRequires one selected value for form submission.
is_read_only(bool)true | falsefalseKeeps roving focus active while preventing value changes.
is_invalid(bool)true | falsefalseMarks the group and each radio control invalid.
variant(FieldVariant)FieldVariant::Primary | FieldVariant::SecondaryFieldVariant::PrimaryPrimary field chrome or the flat surface treatment.
name(value)string—Shared submitted name for the selected radio value.
orientation(Orientation)Orientation::Horizontal | Orientation::VerticalOrientation::VerticalLayout and arrow-key orientation.
new(id, options)PartialAnyElement | RadioGrouprender closure—Compound label, description, radios and field error; root render props are not exposed as one closure.
option_content(render)PartialAnyElement | RadioFieldrender closure—Static option labels and state-driven content are supported, but arbitrary part ordering is not.
indicator(render)AnyElement—Custom content inside the control wrapper.
option_content(render) + indicator(render)true | false—Current option selection supplied to a custom renderer.
Not ported1 HeroUI entry not ported
Radio Group builders not ported
HeroUI v3Why it is not ported
RadioGroup.renderOverrides the group element.

Parts

Radio Group parts
PartDescription
RadioGroupSingle-selection, form, validation and roving-focus owner.
RadioGroupOptional group label with required and invalid state.
RadioGroupGroup-level or per-option help text.
RadioOptionOne option field with its own value and disabled state.
RadioGroupPartialClickable row containing the control and label; arbitrary child ordering is not exposed.
RadioGroupFocusable 16px selection control.
RadioGroupBuilt-in selected dot or caller-rendered indicator.
RadioGroupGroup-level or per-radio validation text outside clickable content.

States

Radio Group states
StateBuilderDescription
Selectedselected == Some(index)Accent control and foreground indicator show the chosen option.
Hoveredinteraction hoveredChanges 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 visibletab_stop_handleThe roving keyboard stop rings the current control.
Pressedinteraction is_pressedThe control shrinks to 95%, the selected fill hovers and the dot grows from 6px to 8px.
Disabled groupRadioGroup::is_disabledEvery option is dimmed, inert and absent from the tab order.
Disabled optionRadioOption::is_disabledOne option is dimmed and skipped by pointer and arrow navigation.
Read onlyis_read_only gates selection after cursor movementArrows move focus while selection remains unchanged.
Invalidself.is_invalid || group error || option errorGroup invalid state outlines every control; a per-radio error outlines only its own control.
VerticalOrientation::VerticalOptions stack and all four radio arrows rove through them.
HorizontalOrientation::HorizontalOptions wrap in a row while radio-group arrows retain four-arrow contract.