Siteplane UI

Radio Group

Choose one option from a related set.

Markdown
"use client";

import { Label } from "@/siteplane/components/label";
import { Radio, RadioGroup } from "@/siteplane/components/radio-group";

export default function Example() {
  return (
    <RadioGroup aria-label="Notification frequency" defaultValue="daily">
      <Label>
        <Radio value="instant" />
        Instantly
      </Label>
      <Label>
        <Radio value="daily" />
        Daily digest
      </Label>
      <Label>
        <Radio value="weekly" />
        Weekly summary
      </Label>
    </RadioGroup>
  );
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/radio-group

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/fieldset @siteplane/form @siteplane/input @siteplane/label

Field is installed with Input.

Usage

import { Label } from "@/siteplane/components/label";
import { Radio, RadioGroup } from "@/siteplane/components/radio-group";

<RadioGroup aria-label="Notification frequency" defaultValue="daily">
  <Label>
    <Radio value="daily" />
    Daily digest
  </Label>
  <Label>
    <Radio value="weekly" />
    Weekly summary
  </Label>
</RadioGroup>;

Composition

RadioGroup
├── Label + Radio
└── Label + Radio

With description

Give each option a FieldItem with the radio beside a column of FieldLabel and FieldDescription, and render the Field as a Fieldset whose legend names the group. Each FieldItem connects its label and description to its radio, so the description is announced separately from the name.

Workspace visibility

Only invited members can see this workspace.

Everyone in your organization can find and join it.

"use client";

import {
  Field,
  FieldDescription,
  FieldItem,
  FieldLabel,
} from "@/siteplane/components/field";
import { Fieldset, FieldsetLegend } from "@/siteplane/components/fieldset";
import { Radio, RadioGroup } from "@/siteplane/components/radio-group";

const options = [
  {
    description: "Only invited members can see this workspace.",
    label: "Private",
    value: "private",
  },
  {
    description: "Everyone in your organization can find and join it.",
    label: "Organization",
    value: "organization",
  },
];

export default function Example() {
  return (
    <Field name="visibility" render={(props) => <Fieldset {...props} />}>
      <FieldsetLegend>Workspace visibility</FieldsetLegend>
      <RadioGroup defaultValue="private">
        {options.map((option) => (
          <FieldItem className="items-start gap-2" key={option.value}>
            <Radio value={option.value} />
            <div className="grid gap-1">
              <FieldLabel>{option.label}</FieldLabel>
              <FieldDescription>{option.description}</FieldDescription>
            </div>
          </FieldItem>
        ))}
      </RadioGroup>
    </Field>
  );
}

Controlled selection

Pass value and onValueChange to own the selected value. A disabled option cannot be selected.

"use client";

import { useState } from "react";
import { Label } from "@/siteplane/components/label";
import { Radio, RadioGroup } from "@/siteplane/components/radio-group";

export default function Example() {
  const [value, setValue] = useState("editor");
  return (
    <RadioGroup
      aria-label="Default role for new members"
      onValueChange={setValue}
      value={value}
    >
      <Label>
        <Radio value="viewer" />
        Viewer
      </Label>
      <Label>
        <Radio value="editor" />
        Editor
      </Label>
      <Label>
        <Radio disabled value="admin" />
        Admin
      </Label>
    </RadioGroup>
  );
}

Card style

Wrap each radio and its text in Label variant="card" so the whole card selects the option.

"use client";

import { Label } from "@/siteplane/components/label";
import { Radio, RadioGroup } from "@/siteplane/components/radio-group";

const plans = [
  {
    description: "€9 per month for individuals and small projects.",
    title: "Starter",
    value: "starter",
  },
  {
    description: "€29 per month for growing teams.",
    title: "Growth",
    value: "growth",
  },
  {
    description: "Custom pricing with single sign-on and audit logs.",
    title: "Enterprise",
    value: "enterprise",
  },
];

export default function Example() {
  return (
    <RadioGroup
      aria-label="Plan"
      className="w-full max-w-sm"
      defaultValue="growth"
    >
      {plans.map((plan) => (
        <Label
          variant="card"
          className="flex w-full items-start"
          key={plan.value}
        >
          <Radio value={plan.value} />
          <span className="flex flex-col gap-1">
            <span>{plan.title}</span>
            <span className="text-muted-foreground text-xs">
              {plan.description}
            </span>
          </span>
        </Label>
      ))}
    </RadioGroup>
  );
}

Form integration

Render the Field as a Fieldset: the legend names the group, and the form submits the selected value under the field’s name.

Billing period
"use client";

import { Button } from "@/siteplane/components/button";
import { Field, FieldItem, FieldLabel } from "@/siteplane/components/field";
import { Fieldset, FieldsetLegend } from "@/siteplane/components/fieldset";
import { Form } from "@/siteplane/components/form";
import { Radio, RadioGroup } from "@/siteplane/components/radio-group";

export default function Example() {
  return (
    <Form
      className="grid w-full max-w-sm gap-4"
      onSubmit={(event) => {
        event.preventDefault();
      }}
    >
      <Field name="billing" render={(props) => <Fieldset {...props} />}>
        <FieldsetLegend>Billing period</FieldsetLegend>
        <RadioGroup defaultValue="monthly">
          <FieldItem>
            <FieldLabel>
              <Radio value="monthly" />
              Monthly
            </FieldLabel>
          </FieldItem>
          <FieldItem>
            <FieldLabel>
              <Radio value="yearly" />
              Yearly
            </FieldLabel>
          </FieldItem>
        </RadioGroup>
      </Field>
      <Button type="submit">Save</Button>
    </Form>
  );
}

Sizes

Set size on the group; each radio inherits it unless it sets its own.

"use client";

import { Label } from "@/siteplane/components/label";
import { Radio, RadioGroup } from "@/siteplane/components/radio-group";

const sizes = [
  ["sm", "small"],
  ["md", "medium"],
  ["lg", "large"],
  ["xl", "extra large"],
] as const;

export default function Example() {
  return (
    <div className="flex flex-wrap gap-8">
      {sizes.map(([size, name]) => (
        <RadioGroup
          aria-label={`Billing period, ${name}`}
          defaultValue="monthly"
          key={size}
          size={size}
        >
          <Label>
            <Radio value="monthly" />
            Monthly
          </Label>
          <Label>
            <Radio value="yearly" />
            Yearly
          </Label>
        </RadioGroup>
      ))}
    </div>
  );
}

API reference

Component properties
PropTypeDefaultDescription
RadioGroup.sizesm | md | lg | xlprovider / lgShared item size.
value / defaultValueunknownuncontrolledControlled or initial selection.
onValueChange(value, eventDetails) => voidunsetSelection change callback.
disabledbooleanfalseDisables the group.
Radio.sizesm | md | lg | xlgroup / provider / lgPer-item dimensions.
Radio.valueunknownrequiredUnique option value.

The group places its options 8 px apart. A gap-* utility in className replaces that, so a horizontal group sets its own, for example className="flex-row gap-4".

RadioProps, RadioGroupProps, RadioPrimitive and RadioGroupPrimitive are exported. radioVariants({ className }) returns the utility classes Radio adds beside its stylesheet. The look and the size come from the kit stylesheet through the attributes Radio renders; the helper does not create radio behavior.

Other props, refs, validation and events follow Base UI RadioGroup. Styled parts accept string className.

Read the installed source.

Usage notes

Tab enters the radio group; arrow keys move among enabled choices. Give both the group and each item an accessible label. Use CheckboxGroup for multiple independent values.

A selected radio fills with the brand accent, and its dot takes the text color for accent fills, --sp-rw-fg-on-accent, which a light brand sets dark (Theming).