Radio Group
Native radio group using fieldset/legend semantics with help text and error messaging.
Examples
Default preview
Radio Group uses native radios: give every option the same name to create the group, and the browser
supplies one-selected-only behavior plus arrow-key movement. The circular control is intentional here because a
radio button is an inherently circular form affordance.
Default + help text
Error + disabled
1<div class="space-y-8">
2 <p class="text-muted-foreground text-sm">
3 Radio Group uses native radios: give every option the same <code>name</code> to create the group, and the browser
4 supplies one-selected-only behavior plus arrow-key movement. The circular control is intentional here because a
5 radio button is an inherently circular form affordance.
6 </p>
7
8 <section class="space-y-4">
9 {{ partial:docs/components/label content="Default + help text" }}
10 <div class="grid gap-6 md:grid-cols-2">
11 {{ partial:components/forms/radio-group legend="Publishing frequency" }}
12 {{ slot:help }}Choose how often subscribers receive updates.{{ /slot:help }}
13 {{ partial:components/forms/radio-option id="frequency-weekly" name="frequency" value="weekly" label="Weekly digest" checked="true" /}}
14 {{ partial:components/forms/radio-option id="frequency-monthly" name="frequency" value="monthly" label="Monthly summary" /}}
15 {{ partial:components/forms/radio-option id="frequency-manual" name="frequency" value="manual" label="Manual only" /}}
16 {{ /partial:components/forms/radio-group }}
17
18 {{ partial:components/forms/radio-group legend="Team size" }}
19 {{ slot:help }}Used to recommend a starter configuration.{{ /slot:help }}
20 {{ partial:components/forms/radio-option id="team-solo" name="team_size" value="solo" label="Solo" /}}
21 {{ partial:components/forms/radio-option id="team-small" name="team_size" value="small" label="2–10 people" checked="true" /}}
22 {{ partial:components/forms/radio-option id="team-large" name="team_size" value="large" label="11+ people" /}}
23 {{ /partial:components/forms/radio-group }}
24 </div>
25 </section>
26
27 <section class="space-y-4">
28 {{ partial:docs/components/label content="Error + disabled" }}
29 <div class="grid gap-6 md:grid-cols-2">
30 {{ partial:components/forms/radio-group legend="Billing plan" error="true" }}
31 {{ slot:error }}Choose a billing plan before continuing.{{ /slot:error }}
32 {{ partial:components/forms/radio-option id="plan-starter" name="plan" value="starter" label="Starter" /}}
33 {{ partial:components/forms/radio-option id="plan-team" name="plan" value="team" label="Team" /}}
34 {{ partial:components/forms/radio-option id="plan-enterprise" name="plan" value="enterprise" label="Enterprise" /}}
35 {{ /partial:components/forms/radio-group }}
36
37 {{ partial:components/forms/radio-group legend="Organization role" disabled="true" }}
38 {{ slot:help }}Roles are managed by your organization admin.{{ /slot:help }}
39 {{ partial:components/forms/radio-option id="role-owner" name="role" value="owner" label="Owner" checked="true" /}}
40 {{ partial:components/forms/radio-option id="role-editor" name="role" value="editor" label="Editor" /}}
41 {{ partial:components/forms/radio-option id="role-viewer" name="role" value="viewer" label="Viewer" /}}
42 {{ /partial:components/forms/radio-group }}
43 </div>
44 </section>
45</div>
<div class="space-y-8">
<p class="text-muted-foreground text-sm">
Radio Group uses native radios: give every option the same <code>name</code> to create the group, and the browser
supplies one-selected-only behavior plus arrow-key movement. The circular control is intentional here because a
radio button is an inherently circular form affordance.
</p>
<section class="space-y-4">
{{ partial:docs/components/label content="Default + help text" }}
<div class="grid gap-6 md:grid-cols-2">
{{ partial:components/forms/radio-group legend="Publishing frequency" }}
{{ slot:help }}Choose how often subscribers receive updates.{{ /slot:help }}
{{ partial:components/forms/radio-option id="frequency-weekly" name="frequency" value="weekly" label="Weekly digest" checked="true" /}}
{{ partial:components/forms/radio-option id="frequency-monthly" name="frequency" value="monthly" label="Monthly summary" /}}
{{ partial:components/forms/radio-option id="frequency-manual" name="frequency" value="manual" label="Manual only" /}}
{{ /partial:components/forms/radio-group }}
{{ partial:components/forms/radio-group legend="Team size" }}
{{ slot:help }}Used to recommend a starter configuration.{{ /slot:help }}
{{ partial:components/forms/radio-option id="team-solo" name="team_size" value="solo" label="Solo" /}}
{{ partial:components/forms/radio-option id="team-small" name="team_size" value="small" label="2–10 people" checked="true" /}}
{{ partial:components/forms/radio-option id="team-large" name="team_size" value="large" label="11+ people" /}}
{{ /partial:components/forms/radio-group }}
</div>
</section>
<section class="space-y-4">
{{ partial:docs/components/label content="Error + disabled" }}
<div class="grid gap-6 md:grid-cols-2">
{{ partial:components/forms/radio-group legend="Billing plan" error="true" }}
{{ slot:error }}Choose a billing plan before continuing.{{ /slot:error }}
{{ partial:components/forms/radio-option id="plan-starter" name="plan" value="starter" label="Starter" /}}
{{ partial:components/forms/radio-option id="plan-team" name="plan" value="team" label="Team" /}}
{{ partial:components/forms/radio-option id="plan-enterprise" name="plan" value="enterprise" label="Enterprise" /}}
{{ /partial:components/forms/radio-group }}
{{ partial:components/forms/radio-group legend="Organization role" disabled="true" }}
{{ slot:help }}Roles are managed by your organization admin.{{ /slot:help }}
{{ partial:components/forms/radio-option id="role-owner" name="role" value="owner" label="Owner" checked="true" /}}
{{ partial:components/forms/radio-option id="role-editor" name="role" value="editor" label="Editor" /}}
{{ partial:components/forms/radio-option id="role-viewer" name="role" value="viewer" label="Viewer" /}}
{{ /partial:components/forms/radio-group }}
</div>
</section>
</div>
Props
| Name | Type | Default | Description |
|---|---|---|---|
legend
|
string
|
|
Required fieldset legend text |
error
|
boolean
|
false
|
Enables destructive visual state and aria-invalid on the fieldset |
disabled
|
boolean
|
false
|
Disables the entire native fieldset |
class
|
string
|
|
Additional classes merged via tw_merge (root element only) |
Source
1{{#
2 @name Radio Group
3 @desc Native radio group using fieldset/legend semantics with help text and error messaging.
4 @param legend string - Required fieldset legend text
5 @param error boolean [false] - Enables destructive visual state and aria-invalid on the fieldset
6 @param disabled boolean [false] - Disables the entire native fieldset
7 @param class string - Additional classes merged via tw_merge (root element only)
8 @slot default - Radio option partials; each option sets the same name attribute to create the native group
9 @slot help - Help text rendered below the options
10 @slot error - Error message rendered below the options and referenced by aria-describedby
11#}}
12{{ _help_slot = slot:help }}
13{{ _error_slot = slot:error }}
14{{ _group_id = legend | dashify }}
15{{ _describedby = '' }}
16{{ if _group_id }}
17 {{ if _error_slot }}
18 {{ _describedby = '{_group_id}-error' }}
19 {{ elseif _help_slot }}
20 {{ _describedby = '{_group_id}-help' }}
21 {{ /if }}
22{{ /if }}
23{{ _fieldset_classes = 'space-y-2 {class}'
24 | tw_merge }}
25{{ if error }}
26 {{ _fieldset_classes = 'space-y-2 [&_[type=radio]]:border-destructive {class}'
27 | tw_merge }}
28{{ /if }}
29{{ _legend_classes = 'text-foreground text-sm font-bold'
30 | tw_merge }}
31{{ if disabled }}
32 {{ _fieldset_classes = 'text-muted-foreground space-y-2 [&_[type=radio]]:border-muted-foreground [&_[type=radio]]:bg-muted {class}'
33 | tw_merge }}
34 {{ _legend_classes = 'text-muted-foreground text-sm font-bold'
35 | tw_merge }}
36{{ /if }}
37<fieldset
38 class="{{ _fieldset_classes }}"
39 {{ if disabled }}disabled{{ /if }}
40 {{ if error }}aria-invalid="true"{{ /if }}
41 {{ if _describedby }}aria-describedby="{{ _describedby }}"{{ /if }}
42>
43 <legend class="{{ _legend_classes }}">
44 {{ legend }}
45 </legend>
46 <div class="space-y-2">
47 {{ slot }}
48 </div>
49 {{ if _error_slot }}
50 <p class="text-destructive text-sm font-bold" id="{{ _group_id }}-error">
51 {{ _error_slot }}
52 </p>
53 {{ elseif _help_slot }}
54 <p class="text-muted-foreground text-sm" id="{{ _group_id }}-help">
55 {{ _help_slot }}
56 </p>
57 {{ /if }}
58</fieldset>
{{#
@name Radio Group
@desc Native radio group using fieldset/legend semantics with help text and error messaging.
@param legend string - Required fieldset legend text
@param error boolean [false] - Enables destructive visual state and aria-invalid on the fieldset
@param disabled boolean [false] - Disables the entire native fieldset
@param class string - Additional classes merged via tw_merge (root element only)
@slot default - Radio option partials; each option sets the same name attribute to create the native group
@slot help - Help text rendered below the options
@slot error - Error message rendered below the options and referenced by aria-describedby
#}}
{{ _help_slot = slot:help }}
{{ _error_slot = slot:error }}
{{ _group_id = legend | dashify }}
{{ _describedby = '' }}
{{ if _group_id }}
{{ if _error_slot }}
{{ _describedby = '{_group_id}-error' }}
{{ elseif _help_slot }}
{{ _describedby = '{_group_id}-help' }}
{{ /if }}
{{ /if }}
{{ _fieldset_classes = 'space-y-2 {class}'
| tw_merge }}
{{ if error }}
{{ _fieldset_classes = 'space-y-2 [&_[type=radio]]:border-destructive {class}'
| tw_merge }}
{{ /if }}
{{ _legend_classes = 'text-foreground text-sm font-bold'
| tw_merge }}
{{ if disabled }}
{{ _fieldset_classes = 'text-muted-foreground space-y-2 [&_[type=radio]]:border-muted-foreground [&_[type=radio]]:bg-muted {class}'
| tw_merge }}
{{ _legend_classes = 'text-muted-foreground text-sm font-bold'
| tw_merge }}
{{ /if }}
<fieldset
class="{{ _fieldset_classes }}"
{{ if disabled }}disabled{{ /if }}
{{ if error }}aria-invalid="true"{{ /if }}
{{ if _describedby }}aria-describedby="{{ _describedby }}"{{ /if }}
>
<legend class="{{ _legend_classes }}">
{{ legend }}
</legend>
<div class="space-y-2">
{{ slot }}
</div>
{{ if _error_slot }}
<p class="text-destructive text-sm font-bold" id="{{ _group_id }}-error">
{{ _error_slot }}
</p>
{{ elseif _help_slot }}
<p class="text-muted-foreground text-sm" id="{{ _group_id }}-help">
{{ _help_slot }}
</p>
{{ /if }}
</fieldset>
Dependencies
Packages
marcorieser/tailwind-merge-statamic
1composer require marcorieser/tailwind-merge-statamic
composer require marcorieser/tailwind-merge-statamic
Internal dependencies
- components/forms/_radio-option (internal partial — copy it alongside this component)
- Slots: default radio-option partials, help, error