Skip to content
Dethink Components

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 files

Import the component into your page or component file.

Usage
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.

Deploy frequency
Show sourceexamples/radio-group/basic.tsx
examples/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.

Horizontal
Disabled group
Show sourceexamples/radio-group/states.tsx
examples/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
examples/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.

RadioGroup props
PropWhat it doesDefault
value / defaultValuestringControlled or uncontrolled selected item value.Not set
onValueChange(value: string) => voidFires with the newly selected item's value.Not set
namestringShared 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 / invalidbooleanGroup-level states inherited by every item.false
RadioGroupItem props
PropWhat it doesDefault
valuestringValue this radio contributes when selected.Not set
controlSize / invalid / disabledper-item overridesItem-level overrides of the group settings.inherited
โ€ฆnative input propsInputHTMLAttributesRenders a real <input type="radio">.Not set