A radio group component that allows users to select a single option from a group of choices. RadioGroup integrates seamlessly with the Form and Field components for automatic data binding and validation. Unlike CheckboxGroup, RadioGroup supports field-level validation (validates on change/blur/input events), providing immediate feedback to users.
import { RadioGroup } from 'frontile';
The most basic usage of the RadioGroup component with a label and options.
import { RadioGroup } from 'frontile';
<template>
<RadioGroup @name='interests' @label='Interests' as |Radio|>
<Radio @label='Music' @value='music' />
<Radio @label='Sports' @value='sports' />
<Radio @label='Technology' @value='technology' />
</RadioGroup>
</template>
Modern Usage: For most use cases, prefer using RadioGroup with the Form and Field components. This provides automatic data binding, validation, and state management without manual
@onChangehandlers.
Field-Level Validation: RadioGroup supports field-level validation that runs on change/blur/input events based on the Form's
@validateOnsetting. This provides immediate feedback as users make selections. This differs from CheckboxGroup, which only validates on form submit.
The recommended pattern for using RadioGroup is with Form and Field components, which provides automatic data binding and state management.
Selected theme:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Form, type FormResultData } from 'frontile';
export default class ControlledRadioGroup extends Component {
@tracked formData = { theme: '' };
handleFormChange = (result: FormResultData) => {
this.formData = result.data;
};
<template>
<div class='flex flex-col gap-4'>
<Form @data={{this.formData}} @onChange={{this.handleFormChange}} as |form|>
<form.Field @name='theme' as |field|>
<field.RadioGroup @label='Theme Preference' as |Radio|>
<Radio @label='Light Mode' @value='light' />
<Radio @label='Dark Mode' @value='dark' />
<Radio @label='System Default' @value='system' />
</field.RadioGroup>
</form.Field>
</Form>
<div class='p-3 border border-neutral-soft rounded'>
<p class='text-sm'>Selected theme: <strong>{{this.formData.theme}}</strong></p>
</div>
</div>
</template>
}
RadioGroup integrates seamlessly with Form validation through Valibot schemas. RadioGroup supports field-level validation that runs on change events based on the @validateOn setting, providing feedback as users make selections.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { array } from '@ember/helper';
import { Form, type FormResultData } from 'frontile';
import { Button } from 'frontile';
import * as v from 'valibot';
const schema = v.object({
fullName: v.pipe(
v.string(),
v.nonEmpty('Full name is required'),
v.minLength(2, 'Name must be at least 2 characters')
),
experience: v.pipe(
v.fallback(v.string(), ''),
v.string(),
v.nonEmpty('Please select your experience level')
),
newsletter: v.boolean()
});
type Schema = v.InferOutput<typeof schema>;
export default class FormValidation extends Component {
@tracked formData: Schema = {
fullName: '',
experience: '',
newsletter: false
};
handleFormChange = (result: FormResultData<Schema>) => {
this.formData = result.data;
};
handleSubmit = (result: FormResultData<Schema>) => {
console.log('Form submitted successfully:', result.data);
};
<template>
<Form
@data={{this.formData}}
@schema={{schema}}
@onChange={{this.handleFormChange}}
@onSubmit={{this.handleSubmit}}
@validateOn={{array 'change' 'submit'}}
as |form|
>
<form.Field @name='fullName' as |field|>
<field.Input @label='Full Name' @isRequired={{true}} />
</form.Field>
<form.Field @name='experience' as |field|>
<field.RadioGroup
@label='Experience Level'
@isRequired={{true}}
as |Radio|
>
<Radio
@label='Junior'
@value='junior'
@description='0-2 years of experience'
/>
<Radio
@label='Mid-level'
@value='mid'
@description='3-5 years of experience'
/>
<Radio
@label='Senior'
@value='senior'
@description='5+ years of experience'
/>
</field.RadioGroup>
</form.Field>
<form.Field @name='newsletter' as |field|>
<field.Checkbox @label='Subscribe to our newsletter' />
</form.Field>
<Button type='submit'>Submit Application</Button>
</Form>
</template>
}
The RadioGroup component supports various optional configurations for layout, sizing, descriptions, and custom styling.
RadioGroup supports vertical (default) and horizontal orientations. This is a RadioGroup-specific feature.
RadioGroup supports descriptions at both the group and individual radio level.
import { RadioGroup } from 'frontile';
import { hash } from '@ember/helper';
<template>
<div class='flex flex-col gap-6'>
{{! Orientation }}
<div>
<h4 class='text-sm font-medium mb-2'>Orientation</h4>
<p class='text-sm text-neutral mb-3'>
RadioGroup supports vertical (default) and horizontal orientations. This is a RadioGroup-specific feature.
</p>
<div class='flex flex-col gap-3'>
<RadioGroup
@name='orientation-vertical'
@label='Vertical Layout (Default)'
@orientation='vertical'
as |Radio|
>
<Radio @label='Option 1' @value='option1' />
<Radio @label='Option 2' @value='option2' />
<Radio @label='Option 3' @value='option3' />
</RadioGroup>
<RadioGroup
@name='orientation-horizontal'
@label='Horizontal Layout'
@orientation='horizontal'
as |Radio|
>
<Radio @label='Small' @value='sm' />
<Radio @label='Medium' @value='md' />
<Radio @label='Large' @value='lg' />
<Radio @label='Extra Large' @value='xl' />
</RadioGroup>
</div>
</div>
{{! Size variants }}
<div>
<h4 class='text-sm font-medium mb-2'>Size Variants</h4>
<div class='flex flex-col gap-3'>
<RadioGroup @name='size-small' @label='Small Size' @size='sm' as |Radio|>
<Radio @label='Option A' @value='a' />
<Radio @label='Option B' @value='b' />
</RadioGroup>
<RadioGroup @name='size-medium' @label='Medium Size' @size='md' as |Radio|>
<Radio @label='Option A' @value='a' />
<Radio @label='Option B' @value='b' />
</RadioGroup>
<RadioGroup @name='size-large' @label='Large Size' @size='lg' as |Radio|>
<Radio @label='Option A' @value='a' />
<Radio @label='Option B' @value='b' />
</RadioGroup>
</div>
</div>
{{! Descriptions }}
<div>
<h4 class='text-sm font-medium mb-2'>Descriptions</h4>
<p class='text-sm text-neutral mb-3'>
RadioGroup supports descriptions at both the group and individual radio level.
</p>
<div class='flex flex-col gap-3'>
<RadioGroup
@name='theme-preference'
@label='Theme Preference'
@description="Choose how you'd like the interface to appear. You can change this later in settings."
as |Radio|
>
<Radio @label='Light' @value='light' />
<Radio @label='Dark' @value='dark' />
<Radio @label='System' @value='system' />
</RadioGroup>
<RadioGroup @name='subscription-plan' @label='Choose your subscription plan' as |Radio|>
<Radio
@label='Basic'
@value='basic'
@description='Perfect for individuals getting started. Includes basic features.'
/>
<Radio
@label='Pro'
@value='pro'
@description='Great for professionals. Includes advanced features and priority support.'
/>
<Radio
@label='Enterprise'
@value='enterprise'
@description='For large teams. Includes all features, dedicated support, and custom integrations.'
/>
</RadioGroup>
</div>
</div>
{{! Custom styling }}
<div>
<h4 class='text-sm font-medium mb-2'>Custom Styling</h4>
<RadioGroup
@name='custom-styled'
@label='Custom Styled Radio Group'
@classes={{hash
base='my-custom-radio-group'
label='my-custom-label'
optionsContainer='my-custom-options-container'
}}
as |Radio|
>
<Radio @label='Option 1' @value='option1' />
<Radio @label='Option 2' @value='option2' />
<Radio @label='Option 3' @value='option3' />
</RadioGroup>
</div>
</div>
</template>
The RadioGroup component follows accessibility best practices:
name attribute ensures radio buttons work as a grouparia-invalid)aria-describedbyElement: <span class="hljs-title class_">HTMLDivElement</span>
| Name | Type | Default | Description |
|---|---|---|---|
classes
|
SlotsToClasses<'label' | 'base' | 'optionsContainer'>
|
- | Class names for each slot of the component, merged with the theme's. |
description
|
string
|
- |
Help text rendered between the label and the control, and referenced by the
ids describedBy returns.
|
errors
|
enum
|
- |
Validation messages for the field. A non-empty value also marks the control
invalid, and an array is joined with ; when displayed.
|
isDisabled
|
boolean
|
false
|
Whether the field is disabled. FormControl passes this through for styling;
the control it wraps is responsible for the disabled attribute.
|
isInvalid
|
boolean
|
false
|
Marks the control invalid without supplying messages, for validation that is reported elsewhere. |
isRequired
|
boolean
|
false
|
Whether the field is required. Adds an asterisk to the label; it does not
set the required attribute on the control itself.
|
label
|
string
|
- |
The label text rendered above the control and associated with it via for.
Use the :label block instead when the label needs markup.
|
name
|
string
|
- | The name attribute applied to every radio in the group, which is what makes the browser treat them as mutually exclusive. |
onChange
|
function
|
- | Callback when the selected radio changes, receiving the new value. |
orientation
|
enum
|
'vertical'
|
How the radios are laid out. |
size
|
enum
|
- | The size applied to every radio in the group and to the group's label. |
value
|
T
|
- |
The currently selected value. Pair with onChange to control the group.
|
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |