RadioGroup
Radio group for single-option selection.
Playground
<RadioGroup
intent="primary"
size="md"
variant="outlined"
>
<RadioItem value="all" label="All notifications" />
<RadioItem value="mentions" label="Mentions only" />
<RadioItem value="none" label="None" />
</RadioGroup>01 Purpose
Reach for a RadioGroup to pick exactly one option from a small set, with every choice visible at once.
| Control | Reach for it when |
|---|---|
| RadioGroup | One choice from two to about five options, all worth showing at once: a plan, a shipping speed, a payment method. |
| Checkbox | Independent on/off toggles, or when more than one option can be selected at the same time. |
| SegmentGroup | One choice from a few short, mutually exclusive options where a compact toggle row reads better than a stacked list. |
| Select | One choice from a long list. Showing every option would crowd the layout, so it collapses into a dropdown. |
02 Examples
Plan picker with descriptions
$state variable with bind:value, then read it back to drive the rest of the form. A plain value sets a starting choice without tracking changes, and name sets the key the value submits under.Selected: pro
<script>
let selectedPlan = $state('pro');
</script>
<RadioGroup label="Choose your plan" name="plan" bind:value={selectedPlan}>
<RadioItem value="free" label="Free" description="3 projects, 1 GB storage, community support" />
<RadioItem value="pro" label="Pro · $12/mo" description="Unlimited projects, 100 GB, priority support" />
<RadioItem value="enterprise" label="Enterprise" description="Custom limits, SLA, dedicated account manager" />
</RadioGroup>Per-item disabled
<RadioGroup label="Shipping speed" value="standard">
<RadioItem value="standard" label="Standard (3–5 days)" />
<RadioItem value="express" label="Express (1–2 days)" disabled />
<RadioItem value="overnight" label="Overnight" />
</RadioGroup>Helper and error
helper or error on the group. When both are present, error replaces helper and marks the group aria-invalid.<RadioGroup label="Frequency" helper="You can change this later in settings">
<RadioItem value="daily" label="Daily" />
<RadioItem value="weekly" label="Weekly" />
</RadioGroup>
<RadioGroup label="Agreement" error="Please select an option to continue">
<RadioItem value="accept" label="I accept" />
<RadioItem value="decline" label="I decline" />
</RadioGroup>03 Customization
Card options via one provider default
<BlocksProvider
defaults={{
RadioItem: {
slotClasses: {
item: 'w-full rounded-contain border border-border-subtle p-3 transition-colors has-[:checked]:border-primary has-[:checked]:bg-primary/10'
}
}
}}
>
<RadioGroup label="Region" value="eu" slotClasses={{ group: 'gap-2' }}>
<RadioItem
value="eu"
label="EU (Frankfurt)"
description="GDPR-friendly, lowest latency in Europe"
/>
<RadioItem
value="us"
label="US (Virginia)"
description="Closest to North American users"
/>
<RadioItem
value="apac"
label="APAC (Singapore)"
description="Lowest latency across Asia-Pacific"
/>
</RadioGroup>
</BlocksProvider>This is one of five ways to restyle a block. See Customization for class, slotClasses, unstyled, preset and provider-level overrides.
04 Accessibility
Built-in ARIA
Every option is a native <input type="radio"> inside a role="radiogroup" container, so a native
form submits the selected value and assistive technology reads the group without extra
wiring. Set name to control the key it submits under. It is
auto-generated otherwise. The group label links via aria-labelledby and helper or error text via aria-describedby. The selected option is marked by a
filled dot.
Keyboard
Tab enters the group and lands on the selected item, or on the first item when
nothing is selected yet. Tab again leaves the group for the next control. Arrow keys move between options and select as they go: Up/Down for
vertical groups, Left/Right for horizontal ones, wrapping at both ends and skipping
disabled items. Focus rings use focus-visible:, so they appear only for keyboard
navigation.
05 API Reference
21 propsProp | Type | Default | Description | |
|---|---|---|---|---|
children required | Snippet | — | RadioItem children to render inside the group. | |
class | string | — | Extra classes merged onto the root wrapper element. | |
disabled | boolean | — | Disable all radio items in the group. | |
error | string | — | Error message below the group. Replaces helper and sets aria-invalid. | |
helper | string | — | Hint text below the group. Hidden when error is set. | |
id | string | — | Explicit id for the group element. Auto-generated if omitted. | |
intent | RadioItemVariants['intent'] | 'primary' | Semantic color propagated to all child RadioItems. | |
label | string | — | Group label displayed above the radio items. | |
mint | MintProp | 'none' | Micro-interaction preset applied to each RadioItem (per-item via context). Only applies while the item is not disabled. | |
name | string | — | Shared name attribute for all radio inputs. Auto-generated if omitted. | |
onValueChange | (value: string) => void | — | Fired after the selected value changes. Receives the new value. | |
orientation | horizontalvertical | 'vertical' | Stack direction for the radio items. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ RadioGroup: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette — presets keep hover/active/dark-mode logic coherent
and make the custom look reusable across the project. | |
required | boolean | — | Require a selection: marks the label with an asterisk, sets aria-required
on the group, and puts the constraint on the radios themselves — so an
empty group blocks the form's submit like a native required control. | |
size | RadioItemVariants['size'] | 'md' | Visual size propagated to all child RadioItems. | |
slotClasses | Partial<Record<RadioGroupSlots, string>> | — | Per-slot class overrides merged with tv() styles. Slots: root (what class
also targets) | group | label | requiredMark | message. | |
tier | InteractiveTier | 'commit' | Semantic radius tier propagated to every RadioItem. Default commit
— radio indicators read as identity circles. Set to modify (or
inherit via TierContext from a wrapping <Toolbar tier="modify">)
for inline-toolbar contexts where a circle feels oversized. | |
unstyled | boolean | — | Remove all default tv() classes. Only user-provided classes apply. | |
value | string | — | Currently selected value. Supports two-way binding via bind:value. | |
variant | RadioItemVariants['variant'] | 'outlined' | Visual weight propagated to all child RadioItems. | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'children' | 'class') |
06 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
RadioGroupProps | interface | props | 0 | — | |
RadioGroupContext | interface | helper | 0 | Reactive context exposed to child RadioItem components via getRadioGroupContext(). | |
RadioItemProps | interface | props | 0 | Individual radio option inside a RadioGroup.
Renders a hidden native <input type="radio"> for form semantics and a styled visual indicator.
The indicator exposes a data-state attribute (checked | unchecked) for CSS-based custom styling. | |
MintProp | type | helper | 1 | — | |
InteractiveTier | type | helper | 1 | Semantic radius tier for interactive surfaces (3-tier system).
- commit → r-human (CTA, identity, status declarations)
- modify → r-interactive (fields, navigation, secondary actions)
Container components (Card, Alert, Toolbar surface, …) live in a third
tier contain (r-structure) which is **not** part of this propagation
context — those surfaces are always r-structure by design and have no
tier-flip use case. | |
RadioGroupSlots | type | variant | 0 | Slot names derived from the tv() config above — single source of truth for slotClasses. | |
RadioGroupVariants | type | variant | 0 | — | |
RadioItemSlots | type | variant | 0 | — | |
RadioItemVariants | type | variant | 0 | — | |
MintName | type | helper | 0 | A mint name: a built-in (autocompleted), 'none' to disable, or any
consumer-registered name. (string & {}) keeps the registry open — a
custom name still type-checks, it just isn't suggested. A typo therefore
also still compiles (it resolves like an unregistered custom name and
warns at runtime); the union buys completion and docs, not validation. | |
MintConfig | interface | helper | 0 | — | |
SlotNames | type | helper | 0 | Extracts the slot-name union from a slotted tv() config function — the
companion to VariantProps. The slot-mode overload returns
(props?) => { [K in keyof S]: SlotFn }, so keyof ReturnType<T> is exactly
the set of slot names a component declares in tv({ slots: … }).
Use it to type a component's slotClasses prop from the single source of
truth (its *.variants.ts) instead of hand-maintaining a parallel union
that silently drifts when a slot is added or renamed: | |
VariantProps | type | helper | 0 | — | |
BuiltinMintName | type | helper | 0 | Built-in mint names as a literal union, so the mint prop autocompletes
across every component — the single list the hand-curated playground knobs
and docs used to drift away from. |
07 Installation
Import
import { RadioGroup, RadioItem } from '@urbicon-ui/blocks';