RadioGroup
A set of checkable buttons where only one can be checked at a time.
Demo
Features
- Full keyboard navigation with roving tabindex
- Vertical and horizontal orientation support
- RTL (Right-to-Left) language support
- Controlled and uncontrolled modes
- Optional loop navigation (wrap from last to first)
- Support for HTML forms (hidden input)
- WCAG compliant with proper ARIA attributes
Installation
dotnet add package SummitUIAnatomy
Import the components and structure them as follows:
<SmRadioGroupRoot>
<SmRadioGroupItem Value="option1">
<SmRadioGroupIndicator>
<span class="dot"></span>
</SmRadioGroupIndicator>
<span>Option 1</span>
</SmRadioGroupItem>
<SmRadioGroupItem Value="option2">
<SmRadioGroupIndicator>
<span class="dot"></span>
</SmRadioGroupIndicator>
<span>Option 2</span>
</SmRadioGroupItem>
</SmRadioGroupRoot>Sub-components
RadioGroupRoot
Container component with radiogroup role. Manages selection state and provides context to child items.
RadioGroupItem
Individual radio button with role="radio". Handles keyboard navigation and selection.
RadioGroupIndicator
Visual indicator that renders when the item is checked. Supports ForceMount for animations.
API Reference
RadioGroupRoot
| Property | Type | Default | Description |
|---|---|---|---|
| Value | string? | null | Controlled selected value |
| DefaultValue | string? | null | Default selected value for uncontrolled mode |
| ValueChanged | EventCallback<string?> | - | Callback when selected value changes |
| ValueExpression | Expression<Func<string?>>? | null | Expression for EditForm binding and validation |
| OnValueChange | EventCallback<string?> | - | Additional callback when value changes |
| Orientation | RadioGroupOrientation | Vertical | Layout orientation (Vertical or Horizontal) |
| Loop | bool | true | Whether navigation loops from last to first |
| Disabled | bool | false | Disable the entire group |
| Required | bool | false | Whether a selection is required |
| Name | string? | null | Form field name for hidden input |
| AriaLabel | string? | null | Accessible label for the group |
| AriaLabelledBy | string? | null | ID of element that labels the group |
| AriaDescribedBy | string? | null | ID of element that describes the group |
RadioGroupItem
| Property | Type | Default | Description |
|---|---|---|---|
| Valuerequired | string | - | Unique value for this radio item |
| Disabled | bool | false | Disable this specific item |
| ChildContent | RenderFragment<SmRadioGroupItemContext>? | - | Content with context providing Checked and Disabled state |
RadioGroupIndicator
| Property | Type | Default | Description |
|---|---|---|---|
| ForceMount | bool | false | Always render in DOM (useful for CSS animations) |
| ChildContent | RenderFragment? | - | Content to render when checked |
Examples
Basic Usage
<SmRadioGroupRoot AriaLabel="Favorite color">
<SmRadioGroupItem Value="red">
<SmRadioGroupIndicator />
<span>Red</span>
</SmRadioGroupItem>
<SmRadioGroupItem Value="green">
<SmRadioGroupIndicator />
<span>Green</span>
</SmRadioGroupItem>
<SmRadioGroupItem Value="blue">
<SmRadioGroupIndicator />
<span>Blue</span>
</SmRadioGroupItem>
</SmRadioGroupRoot>With Default Value
Set an initial selection using DefaultValue for uncontrolled mode.
<SmRadioGroupRoot DefaultValue="medium" AriaLabel="Select size">
<SmRadioGroupItem Value="small">
<SmRadioGroupIndicator />
<span>Small</span>
</SmRadioGroupItem>
<SmRadioGroupItem Value="medium">
<SmRadioGroupIndicator />
<span>Medium</span>
</SmRadioGroupItem>
<SmRadioGroupItem Value="large">
<SmRadioGroupIndicator />
<span>Large</span>
</SmRadioGroupItem>
</SmRadioGroupRoot>Controlled Mode
Control the selection state externally with two-way binding.
@code {
private string? selectedValue = "option1";
}
<SmRadioGroupRoot @bind-Value="selectedValue" AriaLabel="Select option">
<SmRadioGroupItem Value="option1">
<SmRadioGroupIndicator />
<span>Option 1</span>
</SmRadioGroupItem>
<SmRadioGroupItem Value="option2">
<SmRadioGroupIndicator />
<span>Option 2</span>
</SmRadioGroupItem>
</SmRadioGroupRoot>
<p>Selected: @selectedValue</p>Horizontal Orientation
Use horizontal orientation for inline radio groups. Arrow Left/Right navigate between items.
<SmRadioGroupRoot Orientation="RadioGroupOrientation.Horizontal" AriaLabel="Text alignment">
<SmRadioGroupItem Value="left">
<SmRadioGroupIndicator />
<span>Left</span>
</SmRadioGroupItem>
<SmRadioGroupItem Value="center">
<SmRadioGroupIndicator />
<span>Center</span>
</SmRadioGroupItem>
<SmRadioGroupItem Value="right">
<SmRadioGroupIndicator />
<span>Right</span>
</SmRadioGroupItem>
</SmRadioGroupRoot>Disabled Items
Individual items or the entire group can be disabled.
@* Disable entire group *@
<SmRadioGroupRoot Disabled="true" AriaLabel="Disabled group">
<SmRadioGroupItem Value="a"><SmRadioGroupIndicator /><span>Option A</span></SmRadioGroupItem>
<SmRadioGroupItem Value="b"><SmRadioGroupIndicator /><span>Option B</span></SmRadioGroupItem>
</SmRadioGroupRoot>
@* Disable individual items *@
<SmRadioGroupRoot AriaLabel="Partially disabled">
<SmRadioGroupItem Value="available"><SmRadioGroupIndicator /><span>Available</span></SmRadioGroupItem>
<SmRadioGroupItem Value="sold-out" Disabled="true"><SmRadioGroupIndicator /><span>Sold Out</span></SmRadioGroupItem>
<SmRadioGroupItem Value="coming-soon" Disabled="true"><SmRadioGroupIndicator /><span>Coming Soon</span></SmRadioGroupItem>
</SmRadioGroupRoot>Form Integration
Use the Name property to include the selection in form submissions.
<form method="post">
<SmRadioGroupRoot Name="subscription" Required="true" AriaLabel="Subscription plan">
<SmRadioGroupItem Value="monthly">
<SmRadioGroupIndicator />
<span>Monthly - $9/month</span>
</SmRadioGroupItem>
<SmRadioGroupItem Value="yearly">
<SmRadioGroupIndicator />
<span>Yearly - $99/year</span>
</SmRadioGroupItem>
</SmRadioGroupRoot>
<button type="submit">Subscribe</button>
</form>EditForm Integration
Use with Blazor EditForm for model binding and validation support.
@using System.ComponentModel.DataAnnotations
<EditForm Model="model" OnValidSubmit="HandleSubmit">
<DataAnnotationsValidator />
<div class="form-group">
<label>Select a subscription plan:</label>
<SmRadioGroupRoot @bind-Value="model.Plan" AriaLabel="Subscription plan">
<SmRadioGroupItem Value="starter" Context="itemContext">
<SmRadioGroupIndicator />
<span>Starter - $9/month</span>
</SmRadioGroupItem>
<SmRadioGroupItem Value="pro" Context="itemContext">
<SmRadioGroupIndicator />
<span>Pro - $29/month</span>
</SmRadioGroupItem>
<SmRadioGroupItem Value="enterprise" Context="itemContext">
<SmRadioGroupIndicator />
<span>Enterprise - $99/month</span>
</SmRadioGroupItem>
</SmRadioGroupRoot>
<ValidationMessage For="@(() => model.Plan)" />
</div>
<button type="submit">Subscribe</button>
</EditForm>
@code {
private FormModel model = new();
public class FormModel
{
[Required(ErrorMessage = "Please select a subscription plan")]
public string? Plan { get; set; }
}
private void HandleSubmit()
{
// Handle form submission
}
}Styling
Data Attributes
| Attribute | Values | Description |
|---|---|---|
| data-summit-radio-group | Present on root | Identifies the radio group root element |
| data-summit-radio-group-item | Present on items | Identifies radio group item elements |
| data-state | "checked" | "unchecked" | Current selection state of the item |
| data-orientation | "vertical" | "horizontal" | Current orientation of the group/item |
| data-disabled | Present when disabled | Indicates disabled state |
CSS Example
/* Radio group container */
.radio-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.radio-group[data-orientation="horizontal"] {
flex-direction: row;
}
/* Radio item */
.radio-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 0.375rem;
cursor: pointer;
}
.radio-item[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
/* Radio indicator (circle) */
.radio-indicator {
width: 1.25rem;
height: 1.25rem;
border: 2px solid #d4d4d8;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.15s ease;
}
/* Hover state on indicator */
.radio-item:hover:not([data-disabled]) .radio-indicator {
border-color: #0066cc80;
}
/* Focus ring on indicator */
.radio-item:focus-visible .radio-indicator {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
.radio-item[data-state="checked"] .radio-indicator {
border-color: #0066cc;
}
/* Radio dot */
.radio-dot {
width: 0.625rem;
height: 0.625rem;
background-color: #0066cc;
border-radius: 50%;
}Accessibility
Keyboard Navigation
| Key | Action |
|---|---|
| Tab | Moves focus to the selected item, or first item if none selected |
| Space | Selects the focused item |
| ArrowDown | Moves to and selects the next item (vertical orientation) |
| ArrowUp | Moves to and selects the previous item (vertical orientation) |
| ArrowRight | Moves to and selects the next item (horizontal orientation, RTL-aware) |
| ArrowLeft | Moves to and selects the previous item (horizontal orientation, RTL-aware) |
ARIA Attributes
- role:
radiogroupon the root element,radioon each item - aria-checked:
Reflects the current selection state on each item (
trueorfalse) - aria-orientation:
Set on root to indicate
verticalorhorizontallayout - aria-label / aria-labelledby: Provides an accessible name for the group
- aria-disabled: Set when an item or the group is disabled
- aria-required: Set when a selection is required
RTL Support
The component automatically detects RTL mode and adjusts horizontal navigation accordingly. In RTL mode, Arrow Right moves to the previous item and Arrow Left moves to the next item.