Radio Group
A set of checkable buttons where only one can be selected at a time.
Overview
The RadioGroup component allows users to select a single option from a set of mutually exclusive choices. Built on Base UI primitives, it provides a fully accessible radio button group with customizable sizes and flexible styling options.
Usage
import { RadioGroup, RadioGroupItem, Label } from "@tilt-legal/cubitt-components/primitives";<RadioGroup defaultValue="option-1">
<div className="flex items-center gap-2">
<RadioGroupItem value="option-1" id="option-1" />
<Label htmlFor="option-1">Option 1</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option-2" id="option-2" />
<Label htmlFor="option-2">Option 2</Label>
</div>
</RadioGroup>Using Cubitt's URL-state hooks, you can sync the radio group selection with the URL by providing a paramName:
// Selected value synced with ?shipping=express
<RadioGroup paramName="shipping" defaultValue="standard">
<div className="flex items-center gap-2">
<RadioGroupItem value="standard" id="standard" />
<Label htmlFor="standard">Standard Shipping</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="express" id="express" />
<Label htmlFor="express">Express Shipping</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="overnight" id="overnight" />
<Label htmlFor="overnight">Overnight Shipping</Label>
</div>
</RadioGroup>
// Advanced options:
<RadioGroup
paramName="preference"
defaultValue="option-1"
paramClearOnDefault={true} // Remove param when value equals default
paramDebounce={150} // Debounce URL updates
onUrlValueChange={(value) => console.log('Selected:', value)}
>
{/* ... */}
</RadioGroup>Examples
Sizes
Radio groups in different sizes.
import { RadioGroup, RadioGroupItem, Label } from "@tilt-legal/cubitt-components/primitives";
export default function Example() {
return (
<div className="flex flex-col gap-6 items-center">
<RadioGroup defaultValue="sm-1" size="sm" className="flex flex-row gap-4">
<Label className="text-sm" orientation="horizontal">
<RadioGroupItem value="sm-1" id="sm-1" />
Option 1
</Label>
<Label className="text-sm" orientation="horizontal">
<RadioGroupItem value="sm-2" id="sm-2" />
Option 2
</Label>
<Label className="text-sm" orientation="horizontal">
<RadioGroupItem value="sm-3" id="sm-3" />
Option 3
</Label>
</RadioGroup>
<RadioGroup defaultValue="md-1" size="md" className="flex flex-row gap-6">
<Label className="text-md" orientation="horizontal">
<RadioGroupItem value="md-1" id="md-1" />
Option 1
</Label>
<Label className="text-md" orientation="horizontal">
<RadioGroupItem value="md-2" id="md-2" />
Option 2
</Label>
<Label className="text-md" orientation="horizontal">
<RadioGroupItem value="md-3" id="md-3" />
Option 3
</Label>
</RadioGroup>
<RadioGroup defaultValue="lg-1" size="lg" className="flex flex-row gap-6">
<Label className="text-lg" orientation="horizontal">
<RadioGroupItem value="lg-1" id="lg-1" />
Option 1
</Label>
<Label className="text-lg" orientation="horizontal">
<RadioGroupItem value="lg-2" id="lg-2" />
Option 2
</Label>
<Label className="text-lg" orientation="horizontal">
<RadioGroupItem value="lg-3" id="lg-3" />
Option 3
</Label>
</RadioGroup>
</div>
);
}Disabled
Radio buttons can be individually disabled.
import { RadioGroup, RadioGroupItem, Label } from "@tilt-legal/cubitt-components/primitives";
export default function Example() {
return (
<RadioGroup defaultValue="disabled-1">
<div className="flex items-center gap-2">
<RadioGroupItem value="disabled-1" id="disabled-1" />
<Label htmlFor="disabled-1">Enabled option</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="disabled-2" id="disabled-2" disabled />
<Label htmlFor="disabled-2">Disabled (unselected)</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="disabled-3" id="disabled-3" disabled />
<Label htmlFor="disabled-3">Disabled option</Label>
</div>
</RadioGroup>
);
}With Description
Radio buttons with additional descriptive text.
import { RadioGroup, RadioGroupItem, Label } from "@tilt-legal/cubitt-components/primitives";
export default function Example() {
return (
<RadioGroup defaultValue="plan-1" className="gap-4">
<div className="flex items-start gap-3">
<RadioGroupItem value="plan-1" id="plan-1" className="mt-0.5" />
<div className="flex flex-col gap-1">
<Label htmlFor="plan-1">Free Plan</Label>
<p className="text-sm text-muted-foreground">
Perfect for personal projects and small teams.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<RadioGroupItem value="plan-2" id="plan-2" className="mt-0.5" />
<div className="flex flex-col gap-1">
<Label htmlFor="plan-2">Pro Plan</Label>
<p className="text-sm text-muted-foreground">
Advanced features for growing businesses.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<RadioGroupItem value="plan-3" id="plan-3" className="mt-0.5" />
<div className="flex flex-col gap-1">
<Label htmlFor="plan-3">Enterprise Plan</Label>
<p className="text-sm text-muted-foreground">
Custom solutions for large organizations.
</p>
</div>
</div>
</RadioGroup>
);
}Card Style
Radio buttons styled as interactive cards.
import { RadioGroup, RadioGroupItem } from "@tilt-legal/cubitt-components/primitives";
export default function Example() {
return (
<RadioGroup defaultValue="card-1" className="gap-3">
<label
htmlFor="card-1"
className="flex items-start gap-3 rounded-xl border p-4 hover:bg-accent/50 transition-colors has-[:checked]:border-primary has-[:checked]:bg-primary/10 dark:has-[:checked]:border-primary/20 dark:has-[:checked]:bg-primary/10 cursor-pointer"
>
<RadioGroupItem value="card-1" id="card-1" className="mt-0.5" />
<div className="flex flex-col gap-1">
<span className="font-medium">Standard Delivery</span>
<p className="text-sm text-muted-foreground">5-7 business days</p>
</div>
</label>
<label
htmlFor="card-2"
className="flex items-start gap-3 rounded-xl border p-4 hover:bg-accent/50 transition-colors has-[:checked]:border-primary has-[:checked]:bg-primary/10 dark:has-[:checked]:border-primary/20 dark:has-[:checked]:bg-primary/10 cursor-pointer"
>
<RadioGroupItem value="card-2" id="card-2" className="mt-0.5" />
<div className="flex flex-col gap-1">
<span className="font-medium">Express Delivery</span>
<p className="text-sm text-muted-foreground">2-3 business days</p>
</div>
</label>
<label
htmlFor="card-3"
className="flex items-start gap-3 rounded-xl border p-4 hover:bg-accent/50 transition-colors has-[:checked]:border-primary has-[:checked]:bg-primary/10 dark:has-[:checked]:border-primary/20 dark:has-[:checked]:bg-primary/10 cursor-pointer"
>
<RadioGroupItem value="card-3" id="card-3" className="mt-0.5" />
<div className="flex flex-col gap-1">
<span className="font-medium">Overnight Delivery</span>
<p className="text-sm text-muted-foreground">Next business day</p>
</div>
</label>
</RadioGroup>
);
}URL State
Sync radio group selection with the URL. Try selecting an option and refreshing the page.
import { RadioGroup, RadioGroupItem, Label } from "@tilt-legal/cubitt-components/primitives";
export default function Example() {
return (
<RadioGroup
paramName="demo-shipping"
defaultValue="standard"
paramClearOnDefault={true}
>
<div className="flex items-center gap-2">
<RadioGroupItem value="standard" id="url-standard" />
<Label htmlFor="url-standard">Standard Shipping</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="express" id="url-express" />
<Label htmlFor="url-express">Express Shipping</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="overnight" id="url-overnight" />
<Label htmlFor="url-overnight">Overnight Shipping</Label>
</div>
</RadioGroup>
);
}API Reference
RadioGroup
The root container component that manages the radio button group.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | The controlled value of the selected radio button. |
defaultValue | string | - | The default value when uncontrolled. |
onValueChange | (value: string) => void | - | Callback fired when the selected value changes. |
disabled | boolean | false | Whether all radio buttons in the group are disabled. |
name | string | - | The name attribute for the radio group (used in forms). |
required | boolean | false | Whether the radio group is required in a form. |
size | "sm" | "md" | "lg" | "md" | Size variant applied to all child RadioGroupItem components. |
className | string | - | Additional CSS classes for the radio group container. |
data-slot | string | - | Data attribute for styling and testing hooks. |
URL State Props
When provided with a paramName, the radio group will sync its selection with URL parameters via TanStack Router search params.
| Prop | Type | Default | Description |
|---|---|---|---|
paramName | string | — | URL parameter name for syncing state with URL. Enables URL state management. |
paramValue | string | — | Controlled value for the URL parameter. |
onUrlValueChange | (value: string) => void | — | Callback when URL parameter value changes. |
paramClearOnDefault | boolean | true | Remove URL param when value equals default. |
paramThrottle | number | — | Throttle URL updates in milliseconds. |
paramDebounce | number | — | Debounce URL updates in milliseconds. |
RadioGroupItem
Individual radio button within a RadioGroup.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | The unique value for this radio button (required). |
id | string | - | The id attribute for the radio button (used with Label htmlFor). |
disabled | boolean | false | Whether this specific radio button is disabled. |
size | "sm" | "md" | "lg" | - | Size variant (inherits from RadioGroup if not specified). |
className | string | - | Additional CSS classes for the radio button. |
data-slot | string | Data attribute for styling and testing hooks. |
Notes
- Radio buttons must be used within a
RadioGroupcomponent - Each
RadioGroupItemshould have a uniquevalueprop within its group - Use the
idprop onRadioGroupItemandhtmlForonLabelfor proper label association - The
sizeprop onRadioGroupautomatically applies to all child items via Context - Individual
RadioGroupItemcomponents can override the group's size if needed