Radio Group
A set of options where users can select only one choice at a time, commonly used in forms, settings, and questionnaires.
import { component$ } from "@builder.io/qwik";
import { RadioGroup } from "@kunai-consulting/qwik";
export default component$(() => {
return (
<RadioGroup.Root orientation="horizontal" class="radio-group-root">
<RadioGroup.Label>Size</RadioGroup.Label>
{["S", "M", "L", "XL"].map((size) => (
<RadioGroup.Item value={size} key={size} class="radio-group-item">
<RadioGroup.Label>{size}</RadioGroup.Label>
<RadioGroup.Trigger class="radio-group-trigger">
<RadioGroup.Indicator class="radio-group-indicator" />
</RadioGroup.Trigger>
</RadioGroup.Item>
))}
</RadioGroup.Root>
);
});
When to use Radio Group
Radio Group is ideal when:
- Users must choose exactly one option from a list (unlike checkboxes which allow multiple selections)
- You have 2-7 mutually exclusive options (for more options, consider using a Select component)
- Options need to be immediately visible to users
- You need native form integration
Real-world examples:
- Payment method selection in checkout flows
- Account settings (e.g., notification preferences)
- Survey questions with single-choice answers
- Subscription plan selection
Implementation Notes
Radio Group is built on native radio inputs, providing several advantages:
- Works with browser form validation
- Reliable form submission
- Better accessibility out of the box
Features
- WAI-ARIA RadioGroup pattern implementation
- Keyboard navigation with orientation support
- Form integration with hidden native inputs
- Error state handling and validation
- Default value support
- Two-way binding support (bind:value)
- Disabled state for group and individual items
- Custom styling through data attributes
- Description and error message support
- Proper focus management
- Required field validation
- Horizontal and vertical orientation
Anatomy
Part | Description |
---|---|
<RadioGroup.Root> | Root component that manages the radio group's state and behavior |
<RadioGroup.Item> | Container component for individual radio options that provides value context |
<RadioGroup.Trigger> | Interactive button component that handles radio option selection |
<RadioGroup.Indicator> | Visual indicator component that shows the selected state |
<RadioGroup.Label> | Label component for the radio group or individual options |
<RadioGroup.Description> | A description component for the radio group that provides additional context |
<RadioGroup.ErrorMessage> | Component for displaying validation error messages |
<RadioGroup.HiddenInput> | Hidden native input for form integration |
Examples
Basic Usage
Start with this example if you're new to Radio Group. It shows the minimal setup needed for a functional component.
import { component$, useStyles$ } from "@builder.io/qwik";
import { RadioGroup } from "@kunai-consulting/qwik";
import styles from "./radio-group-custom.css?inline";
export default component$(() => {
useStyles$(styles);
return (
<RadioGroup.Root class="radio-group-root">
<RadioGroup.Label>Choose option</RadioGroup.Label>
{["Option 1", "Option 2"].map((value) => (
<RadioGroup.Item value={value} key={value} class="radio-group-item">
<RadioGroup.Label>{value}</RadioGroup.Label>
<RadioGroup.Trigger class="radio-group-trigger">
<RadioGroup.Indicator class="radio-group-indicator" />
</RadioGroup.Trigger>
</RadioGroup.Item>
))}
</RadioGroup.Root>
);
});
This example demonstrates:
- Basic radio group structure
- Label and item organization
- Visual indicators for selection
- Default vertical orientation
<RadioGroup.Root>
<RadioGroup.Item value="option1">
<RadioGroup.Label>Option 1</RadioGroup.Label>
<RadioGroup.Trigger>
<RadioGroup.Indicator />
</RadioGroup.Trigger>
</RadioGroup.Item>
</RadioGroup.Root>
Value Based
Manage the radio group's selected value externally using signals, allowing for dynamic updates and state sharing across components.
import { $, component$, useSignal, useStyles$ } from "@builder.io/qwik";
import { RadioGroup } from "@kunai-consulting/qwik";
import styles from "./radio-group-custom.css?inline";
export default component$(() => {
useStyles$(styles);
const currentValue = useSignal("Option 1");
const valueSelected$ = $((value: string) => {
currentValue.value = value;
console.log("Selected:", value);
});
return (
<RadioGroup.Root
value={currentValue.value}
onChange$={valueSelected$}
class="radio-group-root"
>
<RadioGroup.Label>Choose option</RadioGroup.Label>
{["Option 1", "Option 2"].map((value) => (
<RadioGroup.Item value={value} key={value} class="radio-group-item">
<RadioGroup.Trigger class="radio-group-trigger">
<RadioGroup.Indicator class="radio-group-indicator" />
</RadioGroup.Trigger>
<RadioGroup.Label>{value}</RadioGroup.Label>
</RadioGroup.Item>
))}
</RadioGroup.Root>
);
});
const currentValue = useSignal("option1");
<RadioGroup.Root
value={currentValue.value}
onChange$={(value: string) => {
currentValue.value = value;
console.log("Selected:", value);
}}
>
{/* Radio items */}
</RadioGroup.Root>
Form Integration
Radio group with form validation and error messages.
import { $, component$, useSignal } from "@builder.io/qwik";
import { RadioGroup } from "@kunai-consulting/qwik";
export default component$(() => {
const isError = useSignal(false);
const items = [
{ label: "Basic - $10/month", value: "basic" },
{ label: "Pro - $20/month", value: "pro" }
];
const handleSubmit$ = $((e: SubmitEvent) => {
const form = e.target as HTMLFormElement;
if (!form.checkValidity()) {
isError.value = true;
} else {
isError.value = false;
console.log("Form submitted successfully");
}
});
return (
<form preventdefault:submit noValidate onSubmit$={handleSubmit$}>
<RadioGroup.Root
required
isDescription
isError={isError.value}
name="subscription"
class="radio-group-root"
onChange$={() => {
isError.value = false;
}}
>
<RadioGroup.Label>Subscription Plan</RadioGroup.Label>
<RadioGroup.Description>
Choose your preferred subscription plan
</RadioGroup.Description>
{items.map((item) => (
<RadioGroup.Item value={item.value} key={item.value} class="radio-group-item">
<RadioGroup.Label>{item.label}</RadioGroup.Label>
<RadioGroup.Trigger class="radio-group-trigger">
<RadioGroup.Indicator class="radio-group-indicator" />
</RadioGroup.Trigger>
<RadioGroup.HiddenInput />
</RadioGroup.Item>
))}
{isError.value && (
<RadioGroup.ErrorMessage class="radio-group-error-message">
Please select a subscription plan
</RadioGroup.ErrorMessage>
)}
</RadioGroup.Root>
<button type="submit">Subscribe</button>
</form>
);
});
Essential for collecting user input. This example demonstrates:
- Form validation integration
- Error message handling
- Description text for better context
- Hidden native inputs for reliable form submission
Horizontal Layout
Radio group with horizontal orientation. Use horizontal layout for:
- Compact spaces
- Short option labels
- Small number of choices (2-3 options)
- Binary choices (Yes/No, Enable/Disable)
import { component$ } from "@builder.io/qwik";
import { RadioGroup } from "@kunai-consulting/qwik";
export default component$(() => {
return (
<RadioGroup.Root orientation="horizontal" class="radio-group-root">
<RadioGroup.Label>Size</RadioGroup.Label>
{["S", "M", "L", "XL"].map((size) => (
<RadioGroup.Item value={size} key={size} class="radio-group-item">
<RadioGroup.Label>{size}</RadioGroup.Label>
<RadioGroup.Trigger class="radio-group-trigger">
<RadioGroup.Indicator class="radio-group-indicator" />
</RadioGroup.Trigger>
</RadioGroup.Item>
))}
</RadioGroup.Root>
);
});
Disabled States
Example showing both group-level and individual item disabled states.
import { component$, useSignal } from "@builder.io/qwik";
import { RadioGroup } from "@kunai-consulting/qwik";
export default component$(() => {
const isGroupDisabled = useSignal(false);
return (
<div class="space-y-4">
<button
onClick$={() => (isGroupDisabled.value = !isGroupDisabled.value)}
class="mb-4"
type="button"
>
{isGroupDisabled.value ? "Enable" : "Disable"} group
</button>
<RadioGroup.Root class="radio-group-root" disabled={isGroupDisabled.value}>
<RadioGroup.Label>Choose option</RadioGroup.Label>
<RadioGroup.Item value="option1" class="radio-group-item">
<RadioGroup.Label>Option 1</RadioGroup.Label>
<RadioGroup.Trigger class="radio-group-trigger">
<RadioGroup.Indicator class="radio-group-indicator" />
</RadioGroup.Trigger>
</RadioGroup.Item>
<RadioGroup.Item value="option2" class="radio-group-item">
<RadioGroup.Label>Option 2 (Disabled)</RadioGroup.Label>
<RadioGroup.Trigger class="radio-group-trigger" disabled>
<RadioGroup.Indicator class="radio-group-indicator" />
</RadioGroup.Trigger>
</RadioGroup.Item>
<RadioGroup.Item value="option3" class="radio-group-item">
<RadioGroup.Label>Option 3</RadioGroup.Label>
<RadioGroup.Trigger class="radio-group-trigger">
<RadioGroup.Indicator class="radio-group-indicator" />
</RadioGroup.Trigger>
</RadioGroup.Item>
</RadioGroup.Root>
</div>
);
});
This demonstrates:
- Disabling the entire group
- Individual item disabling
- Visual feedback for disabled state
- Maintaining proper keyboard navigation
Implement disabled states to:
- Prevent selection based on conditions
- Show unavailable options
- Maintain context while restricting access
Keyboard Navigation
The component supports full keyboard navigation:
- Tab: Focus the radio group
- Space/Enter: Select focused option
- Arrow keys: Navigate between options (respects orientation)
- Home/End: Jump to first/last option
Accessibility
Built following the WAI-ARIA radio group pattern, the component includes:
- Proper role attributes (radiogroup, radio)
- Aria states (checked, disabled)
- Focus management
- Keyboard navigation
- Form integration
- Error states
- Description support
Form Integration
The component provides several features for form integration:
<RadioGroup.Root
name="options" // Form field name
required={true} // Required validation
form="form-id" // Form connection
>
{/* Radio items */}
<RadioGroup.ErrorMessage>
Please select an option
</RadioGroup.ErrorMessage>
</RadioGroup.Root>
Styling
The component uses data attributes for styling:
[data-qds-radio-group-root] {
/* Root styles */
}
[data-qds-radio-group-trigger][data-state="checked"] {
/* Checked state styles */
}
[data-qds-radio-group-trigger][data-disabled] {
/* Disabled state styles */
}
API Reference
Radio Group Root
Inherits from: <div />
Props
Prop | Type | Default | Description |
---|---|---|---|
bind:value | Signal<string | undefined> | - | Two-way binding for the selected value |
onChange$ | (value: string) => void | - | Event handler for when the radio group selection changes |
disabled | boolean | - | Whether the radio group is disabled |
isDescription | boolean | - | Whether the radio group has a description |
isError | boolean | - | Whether the radio group is in error state |
name | string | - | Name attribute for form integration |
required | boolean | - | Whether the radio group is required |
orientation | "horizontal" | "vertical" | - | The orientation of the radio group |
value | string | - | The current value of the radio group |
Data Attributes
Attribute | Description |
---|---|
data-disabled | Indicates whether the radio group is disabled |
data-orientation | The orientation of the radio group |
Radio Group Item
Inherits from: <div />
Props
Prop | Type | Default | Description |
---|---|---|---|
value | string | - | The value of the radio item |
Data Attributes
Attribute | Description |
---|---|
data-orientation | The orientation of the radio group item |
Radio Group Trigger
Inherits from: <button />
Data Attributes
Attribute | Description |
---|---|
data-disabled | Indicates whether this trigger is disabled |
data-checked | Indicates whether this trigger is checked |
Radio Group Error Message
Inherits from: <div />
Data Attributes
Attribute | Description |
---|---|
data-visible | Indicates whether the error message is currently visible |
Radio Group Indicator
Inherits from: <span />
Data Attributes
Attribute | Description |
---|---|
data-checked | Indicates whether this indicator is in a checked state |
Radio Group Description
Inherits from: <div />
Radio Group Hidden Input
Inherits from: <input />
Radio Group Label
Inherits from: <span />
Accessibility
Keyboard Interactions
Key | Description |
---|---|
Space | Selects the focused radio option |
Enter | Selects the focused radio option |
ArrowDown | Moves focus to the next enabled radio option in vertical orientation |
ArrowRight | Moves focus to the next enabled radio option in horizontal orientation |
ArrowUp | Moves focus to the previous enabled radio option in vertical orientation |
ArrowLeft | Moves focus to the previous enabled radio option in horizontal orientation |
Home | Moves focus to the first enabled radio option |
End | Moves focus to the last enabled radio option |