ComponentsRadioGroup
RadioGroup
Let users choose one option from a visible list.
On this page
Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.
View registry filesImport the component into your page or component file.
import { RadioGroup, RadioGroupItem } from "@dethink/components";Try the examples, then open the code to use them in your app. Arrow keys move selection inside a focused group.
With FieldSet anatomy
FieldSet and FieldLegend name the group; each item pairs a RadioGroupItem with a FieldLabel.
Show sourceexamples/radio-group/basic.tsx
"use client";
import {
Field,
FieldControl,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
RadioGroup,
RadioGroupItem,
} from "@dethink/components";
export function RadioGroupBasic() {
return (
<div className="mx-auto max-w-sm">
<FieldSet>
<FieldLegend>Deploy frequency</FieldLegend>
<RadioGroup name="frequency" defaultValue="on-merge">
<FieldGroup>
<Field id="freq-on-merge" orientation="horizontal">
<FieldControl asChild>
<RadioGroupItem value="on-merge" />
</FieldControl>
<FieldLabel>Deploy on every merge</FieldLabel>
</Field>
<Field id="freq-daily" orientation="horizontal">
<FieldControl asChild>
<RadioGroupItem value="daily" />
</FieldControl>
<FieldLabel>Once a day</FieldLabel>
</Field>
<Field id="freq-manual" orientation="horizontal">
<FieldControl asChild>
<RadioGroupItem value="manual" />
</FieldControl>
<FieldLabel>Manual releases only</FieldLabel>
</Field>
</FieldGroup>
</RadioGroup>
</FieldSet>
</div>
);
}Orientation and disabled
orientation lays items out horizontally; group-level disabled flows to every item.
Show sourceexamples/radio-group/states.tsx
"use client";
import {
Field,
FieldControl,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
RadioGroup,
RadioGroupItem,
} from "@dethink/components";
export function RadioGroupStates() {
return (
<div className="mx-auto grid max-w-lg gap-6 sm:grid-cols-2">
<FieldSet>
<FieldLegend>Horizontal</FieldLegend>
<RadioGroup
name="priority"
orientation="horizontal"
defaultValue="high"
>
<FieldGroup className="flex-row gap-4">
<Field id="pri-low" orientation="horizontal">
<FieldControl asChild>
<RadioGroupItem value="low" />
</FieldControl>
<FieldLabel>Low</FieldLabel>
</Field>
<Field id="pri-high" orientation="horizontal">
<FieldControl asChild>
<RadioGroupItem value="high" />
</FieldControl>
<FieldLabel>High</FieldLabel>
</Field>
</FieldGroup>
</RadioGroup>
</FieldSet>
<FieldSet>
<FieldLegend>Disabled group</FieldLegend>
<RadioGroup name="tier" disabled defaultValue="team">
<FieldGroup>
<Field id="tier-starter" orientation="horizontal">
<FieldControl asChild>
<RadioGroupItem value="starter" />
</FieldControl>
<FieldLabel>Starter</FieldLabel>
</Field>
<Field id="tier-team" orientation="horizontal">
<FieldControl asChild>
<RadioGroupItem value="team" />
</FieldControl>
<FieldLabel>Team</FieldLabel>
</Field>
</FieldGroup>
</RadioGroup>
</FieldSet>
</div>
);
}Examples that combine components for common tasks.
Card-style plan picker
Each label grows into a selectable card while the radio stays a real focusable input โ so clicks, keyboard movement, and announcements all remain native RadioGroup behavior.
Selected: Team
Show sourceexamples/radio-group/recipe-plan-picker.tsx
"use client";
import { useState } from "react";
import { RadioGroup, RadioGroupItem } from "@dethink/components";
const plans = [
{
value: "starter",
name: "Starter",
price: "$0",
blurb: "Two projects, community support.",
},
{
value: "team",
name: "Team",
price: "$29",
blurb: "Unlimited projects, shared themes.",
},
{
value: "scale",
name: "Scale",
price: "$99",
blurb: "SSO, audit log, priority support.",
},
];
/**
* The radio stays a real, focusable input โ the surrounding label just grows
* into a card. Selection styling hangs off the same state via peer classes,
* so keyboard behavior is native RadioGroup behavior.
*/
export function RadioGroupRecipePlanPicker() {
const [plan, setPlan] = useState("team");
return (
<div className="mx-auto max-w-md space-y-3">
<RadioGroup
name="plan"
aria-label="Pricing plan"
value={plan}
onValueChange={setPlan}
className="grid gap-3"
>
{plans.map((entry) => (
<label
key={entry.value}
className={`flex cursor-pointer items-start gap-3 rounded-lg border p-4 transition-colors ${
plan === entry.value
? "border-primary bg-primary/[0.06]"
: "border-border hover:border-primary/40"
}`}
>
<RadioGroupItem value={entry.value} className="mt-1" />
<span className="flex-1">
<span className="flex items-baseline justify-between">
<span className="font-heading font-semibold">{entry.name}</span>
<span className="text-muted-foreground text-sm">
{entry.price}/mo
</span>
</span>
<span className="text-muted-foreground mt-1 block text-sm">
{entry.blurb}
</span>
</span>
</label>
))}
</RadioGroup>
<p className="text-muted-foreground text-sm">
Selected: {plans.find((entry) => entry.value === plan)?.name}
</p>
</div>
);
}RadioGroup renders a div with radiogroup semantics; items are real radio inputs.
| Prop | What it does | Default |
|---|---|---|
value / defaultValuestring | Controlled or uncontrolled selected item value. | Not set |
onValueChange(value: string) => void | Fires with the newly selected item's value. | Not set |
namestring | Shared input name applied to every item in the group. | Not set |
orientation"vertical" | "horizontal" | Layout direction and arrow-key axis. | "vertical" |
controlSize"sm" | "md" | "lg" | Control size inherited by every item. | "md" |
required / disabled / readOnly / invalidboolean | Group-level states inherited by every item. | false |
| Prop | What it does | Default |
|---|---|---|
valuestring | Value this radio contributes when selected. | Not set |
controlSize / invalid / disabledper-item overrides | Item-level overrides of the group settings. | inherited |
โฆnative input propsInputHTMLAttributes | Renders a real <input type="radio">. | Not set |