Skip to content
Dethink Components

ComponentsRadioGroup

RadioGroup

Let users choose one option from a visible list.

On this page

Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.

View registry files

Implemented with React and browser APIs, without an additional behavior or rendering library.

Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.

Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.

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 { useId, 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 id = useId();
  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}
            htmlFor={`${id}-${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
              id={`${id}-${entry.value}`}
              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