# Radio Component
Styled radio button inputs with labels, descriptions, and various states. Perfect for forms, settings pages, and single-select options within a group.
## Features
- Multiple sizes (small, medium, large)
- Label and optional description support
- Disabled and required states
- Error message display
- Dark mode support
- Zero JavaScript required
- Accessibility-friendly markup
- Form integration with name/value attributes
- Theme-aware semantic primary color
- Customizable styling via class overrides
- Radio groups (multiple radios with same name)
## Implementation Options
| Format | Location | Best For |
| ------ | -------- | -------- |
| **Plain ERB** | `app/views/components/radio/` | Full control, copy-paste |
| **Shared Partial** | `app/views/shared/components/radio/` | Reusable partial with locals |
| **ViewComponent** | `app/components/radio/` | Ruby-based, testable, object-oriented |
### Plain ERB
Copy the radio HTML directly into your views for maximum flexibility and customization. Best when you need one-off radios or want to modify the markup.
### Shared Partial
Use the reusable partial for consistent radio buttons across your app:
```erb
<%= render "shared/components/radio/radio",
label: "Option A",
name: "selection",
value: "a",
checked: true %>
```
### ViewComponent
Use the object-oriented ViewComponent approach for better testing and Ruby-based logic:
```erb
<%= render Radio::Component.new(
label: "Option A",
name: "selection",
value: "a",
checked: true
) %>
```
---
## Options Reference
| Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| `label` | String | **required** | The radio button label text |
| `name` | String | `nil` | The input name attribute for form submission (radios with same name form a group) |
| `id` | String | auto-generated | The input id attribute |
| `value` | String | `nil` | The input value attribute |
| `checked` | Boolean | `false` | Whether the radio button is selected |
| `disabled` | Boolean | `false` | Whether the radio button is disabled |
| `required` | Boolean | `false` | Whether the radio button is required |
| `description` | String | `nil` | Optional description text below the label |
| `size` | Symbol/String | `:md` / `"md"` | Size: `:sm`, `:md`, `:lg` |
| `error` | String | `nil` | Error message to display |
| `classes` | String | `nil` | Additional CSS classes for the wrapper |
| `input_classes` | String | `nil` | Additional CSS classes for the input |
| `label_classes` | String | `nil` | Additional CSS classes for the label |
---
## Theming
The Rails Blocks installation CSS defines `primary`, `primary-content`, and `accent` semantic tokens. Selected fills, focus rings, and native accent color follow those tokens, including generated Theme Builder recipes in light and dark mode.
Keep status colors such as red or amber for validation and status UI. To customize the brand color, set the `--rb-primary`, `--rb-primary-content`, and related variables rather than adding numbered `primary-*` utilities.
---
## Basic Examples
### Simple Radio Button
```erb
<%= render "shared/components/radio/radio",
label: "Select this option",
name: "option",
value: "selected" %>
```
### Radio Button Group
Radio buttons with the same `name` form a group where only one can be selected:
```erb
<fieldset class="space-y-3">
<legend class="text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-4">
Payment Method
</legend>
<%= render "shared/components/radio/radio",
label: "Credit Card",
name: "payment",
value: "card",
checked: true %>
<%= render "shared/components/radio/radio",
label: "PayPal",
name: "payment",
value: "paypal" %>
<%= render "shared/components/radio/radio",
label: "Bank Transfer",
name: "payment",
value: "bank" %>
</fieldset>
```
### Disabled
```erb
<%= render "shared/components/radio/radio",
label: "Coming soon (disabled)",
name: "feature",
value: "disabled",
disabled: true %>
```
### Required
```erb
<%= render "shared/components/radio/radio",
label: "Accept terms",
name: "accept_terms",
value: "yes",
required: true %>
```
---
## With Description
For radio buttons that need additional context:
```erb
<%= render "shared/components/radio/radio",
label: "Professional Plan",
name: "plan",
value: "professional",
description: "Advanced features for growing businesses and teams" %>
```
**ViewComponent:**
```erb
<%= render Radio::Component.new(
label: "Professional Plan",
name: "plan",
value: "professional",
description: "Advanced features for growing businesses and teams"
) %>
```
---
## Sizes
### Small
```erb
<%= render "shared/components/radio/radio",
label: "Compact option",
name: "size_demo",
value: "sm",
size: "sm" %>
```
### Medium (Default)
```erb
<%= render "shared/components/radio/radio",
label: "Standard option",
name: "size_demo",
value: "md",
size: "md" %>
```
### Large
```erb
<%= render "shared/components/radio/radio",
label: "Prominent option",
name: "size_demo",
value: "lg",
size: "lg" %>
```
---
## Error States
Display validation errors alongside the radio button:
```erb
<%= render "shared/components/radio/radio",
label: "Select this option",
name: "required_selection",
value: "option",
error: "Please select an option to continue" %>
```
---
## Form Integration
### With Rails Form Builder
```erb
<%= form_with model: @order do |f| %>
<fieldset class="space-y-3">
<legend class="text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-4">
Shipping Method
</legend>
<%= render "shared/components/radio/radio",
label: "Standard Shipping (5-7 days)",
name: "order[shipping]",
value: "standard",
checked: @order.shipping == "standard" %>
<%= render "shared/components/radio/radio",
label: "Express Shipping (2-3 days)",
name: "order[shipping]",
value: "express",
checked: @order.shipping == "express" %>
<%= render "shared/components/radio/radio",
label: "Overnight Shipping (1 day)",
name: "order[shipping]",
value: "overnight",
checked: @order.shipping == "overnight" %>
</fieldset>
<% end %>
```
### Subscription Plans
```erb
<fieldset class="space-y-4">
<legend class="text-sm font-semibold text-neutral-900 dark:text-neutral-100 mb-4">
Choose Your Plan
</legend>
<%= render "shared/components/radio/radio",
label: "Basic - $9/month",
name: "plan",
value: "basic",
description: "Perfect for individuals getting started" %>
<%= render "shared/components/radio/radio",
label: "Professional - $29/month",
name: "plan",
value: "professional",
description: "Advanced features for growing businesses",
checked: true %>
<%= render "shared/components/radio/radio",
label: "Enterprise - $99/month",
name: "plan",
value: "enterprise",
description: "Full-featured solution with priority support" %>
</fieldset>
```
---
## Customization
### Custom Wrapper Styling
```erb
<%= render "shared/components/radio/radio",
label: "Highlighted option",
name: "highlighted",
value: "highlight",
classes: "p-4 bg-neutral-50 dark:bg-neutral-800 rounded-lg border border-neutral-200 dark:border-neutral-700" %>
```
### Custom Input Color
```erb
<%= render "shared/components/radio/radio",
label: "Blue radio",
name: "color_demo",
value: "blue",
input_classes: "text-blue-600 focus:ring-blue-500 dark:checked:bg-blue-600" %>
```
### Custom Label Styling
```erb
<%= render "shared/components/radio/radio",
label: "Bold label",
name: "label_demo",
value: "bold",
label_classes: "font-bold text-neutral-900 dark:text-white" %>
```
---
## Accessibility
### Semantic Considerations
- Labels are properly associated with inputs via `for` attribute
- Required fields show visual indicator (*)
- Error messages are displayed adjacent to the input
- Disabled states use appropriate styling and cursor
- Use `<fieldset>` and `<legend>` to group related radio buttons
### Example with ARIA
For dynamic error messages, consider adding ARIA attributes:
```erb
<fieldset aria-describedby="plan-error">
<legend class="text-sm font-medium text-neutral-900 dark:text-neutral-100">
Select a plan <span class="text-red-500">*</span>
</legend>
<div class="flex items-center gap-2 mt-2">
<input
type="radio"
id="plan-basic"
name="plan"
value="basic"
aria-invalid="true"
class="size-4 rounded-full border-red-500 text-neutral-900 focus:ring-red-500"
>
<label for="plan-basic" class="text-sm font-medium text-red-700">
Basic
</label>
</div>
<p id="plan-error" class="text-xs text-red-600 mt-2">
Please select a plan to continue
</p>
</fieldset>
```
---
## Size Reference
| Size | Input | Label | Description |
| ---- | ----- | ----- | ----------- |
| `sm` | `size-3.5` (14px) | `text-xs` | `text-[11px]` |
| `md` | `size-4` (16px) | `text-sm` | `text-xs` |
| `lg` | `size-5` (20px) | `text-base` | `text-sm` |
---
## Best Practices
1. **Use clear labels**: Labels should clearly describe what selecting the option does
2. **Group related options**: Use fieldsets with legends for radio groups
3. **Mark required fields**: Use the `required` option for mandatory selections
4. **Provide descriptions**: Add context for complex options
5. **Handle errors gracefully**: Display validation errors clearly
6. **Consider mobile**: Ensure touch targets are large enough (use `lg` size if needed)
7. **Test dark mode**: Ensure radio buttons remain visible in both themes
8. **Default selection**: Consider pre-selecting the most common option
9. **Limit options**: Keep radio groups to 5-7 options max; use a select/dropdown for more
---
## Common Use Cases
### Survey Question
```erb
<fieldset class="space-y-3">
<legend class="text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-4">
How satisfied are you with our service?
</legend>
<% %w[very_satisfied satisfied neutral dissatisfied very_dissatisfied].each do |rating| %>
<%= render "shared/components/radio/radio",
label: rating.humanize,
name: "satisfaction",
value: rating,
size: "sm" %>
<% end %>
</fieldset>
```
### Settings Panel
```erb
<div class="space-y-6">
<h3 class="text-sm font-semibold text-neutral-900 dark:text-neutral-100">
Email Frequency
</h3>
<fieldset class="space-y-4">
<legend class="sr-only">Email frequency</legend>
<%= render "shared/components/radio/radio",
label: "Real-time",
name: "email_frequency",
value: "realtime",
description: "Get notified immediately for all updates" %>
<%= render "shared/components/radio/radio",
label: "Daily digest",
name: "email_frequency",
value: "daily",
description: "Receive a daily summary of activity",
checked: @user.email_frequency == "daily" %>
<%= render "shared/components/radio/radio",
label: "Weekly digest",
name: "email_frequency",
value: "weekly",
description: "Receive a weekly summary of activity" %>
<%= render "shared/components/radio/radio",
label: "Never",
name: "email_frequency",
value: "never",
description: "Don't send me any email notifications" %>
</fieldset>
</div>
```
### Filter Options
```erb
<fieldset class="space-y-2">
<legend class="sr-only">Sort by</legend>
<% [
["newest", "Newest first"],
["oldest", "Oldest first"],
["popular", "Most popular"],
["alphabetical", "A to Z"]
].each do |value, label| %>
<%= render "shared/components/radio/radio",
label: label,
name: "sort",
value: value,
checked: params[:sort] == value,
size: "sm" %>
<% end %>
</fieldset>
```
---
## AI Instructions
### Choose An Implementation
- **Vanilla / plain ERB**: Use when you want full markup control or need to adapt the example directly inside a page.
- **shared partial**: Use when you want a reusable partial with locals and a consistent render call across views.
- **ViewComponent**: Use when you want a Ruby API, slots, stronger encapsulation, or repeated composition in multiple places.
### Quick Reference
- **Vanilla examples**: `app/views/components/radio/`
- **Shared partial files**: `app/views/shared/components/radio/`
- **shared partial**: `render "shared/components/radio/radio"`
- **ViewComponent**: `render Radio::Component.new(...)`
- **ViewComponent files**: `app/components/radio/`
### Implementation Checklist
- Pick one implementation path first, then stay consistent within that example.
- Use only documented locals, initializer arguments, variants, and slot names.
- Copy the base example before adding app-specific styling or behavior.
- If the component is interactive, keep the documented Stimulus controller, targets, values, and ARIA wiring intact.
- Keep variant names, enum values, and option formats exactly as documented.
### Common Patterns
- **Vanilla / plain ERB**: Best for one-off pages, direct markup edits, and quick adaptations of the shipped examples.
- **shared partial**: Best for reusable Rails view partials driven by locals or collections.
- **ViewComponent**: Best for reusable component implementations that benefit from Ruby-side defaults, slots, or testable composition.
### Common Mistakes
- Do not mix shared partial locals with ViewComponent initializer arguments in the same example.
- Do not invent undocumented variants, sizes, slot names, or helper methods.
- Do not remove required wrapper elements, IDs, or data attributes when copying the component.
- Do not swap the documented render path for a guessed partial or component name.