Skip to main content
PolyUI/docs

表单组件

RadioGroup 单选按钮组

单选按钮组,允许用户从一组互斥选项中选择一项。基于 Base UI Radio 构建,完整支持键盘导航。

安装

bash
npx polyui add radio-group

基础用法

tsx
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
export function RadioGroupUsage() {
  const frameworks = ["React", "Vue", "Svelte", "Solid"]
  return (
    <RadioGroup defaultValue="react">
      {frameworks.map((fw) => (
        <div key={fw} className="flex items-center gap-2">
          <RadioGroupItem value={fw.toLowerCase()} id={`usage-${fw}`} />
          <Label htmlFor={`usage-${fw}`}>{fw}</Label>
        </div>
      ))}
    </RadioGroup>
  )
}

禁用

Entire group disabled

Single item disabled

tsx
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
export function RadioGroupDisabled() {
  return (
    <div className="flex flex-col gap-6">
      <div>
        <p className="text-muted-foreground mb-2 text-xs">Entire group disabled</p>
        <RadioGroup disabled defaultValue="react">
          <div className="flex items-center gap-2">
            <RadioGroupItem value="react" id="dis-r1" />
            <Label htmlFor="dis-r1">React</Label>
          </div>
          <div className="flex items-center gap-2">
            <RadioGroupItem value="vue" id="dis-r2" />
            <Label htmlFor="dis-r2">Vue</Label>
          </div>
        </RadioGroup>
      </div>
      <div>
        <p className="text-muted-foreground mb-2 text-xs">Single item disabled</p>
        <RadioGroup defaultValue="react">
          <div className="flex items-center gap-2">
            <RadioGroupItem value="react" id="dis-r3" />
            <Label htmlFor="dis-r3">React</Label>
          </div>
          <div className="flex items-center gap-2">
            <RadioGroupItem value="vue" id="dis-r4" disabled />
            <Label htmlFor="dis-r4">Vue</Label>
          </div>
        </RadioGroup>
      </div>
    </div>
  )
}

默认

tsx
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
export function RadioGroupDefault() {
  return (
    <RadioGroup defaultValue="higher-secondary">
      <div className="flex items-center gap-2">
        <RadioGroupItem value="higher-secondary" id="higher-secondary" />
        <Label htmlFor="higher-secondary">Higher Secondary</Label>
      </div>
      <div className="flex items-center gap-2">
        <RadioGroupItem value="graduation" id="graduation" />
        <Label htmlFor="graduation">Graduation</Label>
      </div>
      <div className="flex items-center gap-2">
        <RadioGroupItem value="post-graduation" id="post-graduation" />
        <Label htmlFor="post-graduation">Post Graduation</Label>
      </div>
    </RadioGroup>
  )
}

水平

tsx
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
export function RadioGroupHorizontal() {
  return (
    <RadioGroup defaultValue="beginner" className="flex items-center gap-4">
      <div className="flex items-center gap-2">
        <RadioGroupItem value="beginner" id="beginner" />
        <Label htmlFor="beginner">Beginner</Label>
      </div>
      <div className="flex items-center gap-2">
        <RadioGroupItem value="intermediate" id="intermediate" />
        <Label htmlFor="intermediate">Intermediate</Label>
      </div>
      <div className="flex items-center gap-2">
        <RadioGroupItem value="advanced" id="advanced" />
        <Label htmlFor="advanced">Advanced</Label>
      </div>
    </RadioGroup>
  )
}

颜色

tsx
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
export function RadioGroupColors() {
  return (
    <RadioGroup defaultValue="destructive" className="flex items-center gap-4">
      <div className="flex items-center gap-2">
        <RadioGroupItem
          value="destructive"
          id="color-destructive"
          className="border-destructive text-destructive focus-visible:ring-destructive/20 focus-visible:border-destructive dark:focus-visible:ring-destructive/40 data-checked:border-destructive data-checked:bg-destructive"
        />
        <Label htmlFor="color-destructive">Destructive</Label>
      </div>
      <div className="flex items-center gap-2">
        <RadioGroupItem
          value="success"
          id="color-success"
          className="border-green-600 text-green-600 focus-visible:border-green-600 focus-visible:ring-green-600/20 dark:border-green-400 dark:text-green-400 dark:focus-visible:border-green-400 dark:focus-visible:ring-green-400/40 data-checked:border-green-600 data-checked:bg-green-600 dark:data-checked:border-green-400 dark:data-checked:bg-green-400"
        />
        <Label htmlFor="color-success">Success</Label>
      </div>
      <div className="flex items-center gap-2">
        <RadioGroupItem
          value="info"
          id="color-info"
          className="border-sky-600 text-sky-600 focus-visible:border-sky-600 focus-visible:ring-sky-600/20 dark:border-sky-400 dark:text-sky-400 dark:focus-visible:border-sky-400 dark:focus-visible:ring-sky-400/40 data-checked:border-sky-600 data-checked:bg-sky-600 dark:data-checked:border-sky-400 dark:data-checked:bg-sky-400"
        />
        <Label htmlFor="color-info">Info</Label>
      </div>
    </RadioGroup>
  )
}

尺寸

tsx
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
export function RadioGroupSizes() {
  return (
    <RadioGroup defaultValue="default" className="flex items-center gap-4">
      <div className="flex items-center gap-2">
        <RadioGroupItem value="default" id="size-default" />
        <Label htmlFor="size-default">Default</Label>
      </div>
      <div className="flex items-center gap-2">
        <RadioGroupItem value="medium" id="size-medium" className="size-5 [&_svg]:size-3" />
        <Label htmlFor="size-medium">Medium</Label>
      </div>
      <div className="flex items-center gap-2">
        <RadioGroupItem value="large" id="size-large" className="size-6 [&_svg]:size-3.5" />
        <Label htmlFor="size-large">Large</Label>
      </div>
    </RadioGroup>
  )
}

虚线

tsx
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
export function RadioGroupDashed() {
  return (
    <RadioGroup defaultValue="standard">
      <div className="flex items-center gap-2">
        <RadioGroupItem
          value="standard"
          id="standard"
          className="border-primary focus-visible:border-primary border-dashed"
        />
        <Label htmlFor="standard">Standard Shipping</Label>
      </div>
      <div className="flex items-center gap-2">
        <RadioGroupItem
          value="express"
          id="express"
          className="border-primary focus-visible:border-primary border-dashed"
        />
        <Label htmlFor="express">Express Delivery</Label>
      </div>
      <div className="flex items-center gap-2">
        <RadioGroupItem
          value="overnight"
          id="overnight"
          className="border-primary focus-visible:border-primary border-dashed"
        />
        <Label htmlFor="overnight">Overnight Shipping</Label>
      </div>
    </RadioGroup>
  )
}

实色

tsx
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
export function RadioGroupSolid() {
  return (
    <RadioGroup defaultValue="light">
      {["light", "dark", "system"].map((theme) => (
        <div key={theme} className="flex items-center gap-2">
          <RadioGroupItem
            value={theme}
            id={`theme-${theme}`}
            className="text-primary-foreground data-checked:bg-primary! data-checked:border-primary"
          />
          <Label htmlFor={`theme-${theme}`}>
            {theme === "light" ? "Light Theme" : theme === "dark" ? "Dark Theme" : "System Default"}
          </Label>
        </div>
      ))}
    </RadioGroup>
  )
}

带描述

Perfect for individuals and small projects.

Advanced features for growing teams.

Full-featured solution for large organizations.

tsx
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
export function RadioGroupDescription() {
  const plans = [
    {
      value: "basic",
      label: "Basic Plan",
      desc: "Perfect for individuals and small projects.",
    },
    {
      value: "pro",
      label: "Pro Plan",
      desc: "Advanced features for growing teams.",
    },
    {
      value: "enterprise",
      label: "Enterprise Plan",
      desc: "Full-featured solution for large organizations.",
    },
  ]
  return (
    <RadioGroup defaultValue="basic">
      {plans.map(({ value, label, desc }) => (
        <div key={value} className="flex gap-2">
          <RadioGroupItem value={value} id={`plan-${value}`} />
          <div className="grid flex-1 space-y-2">
            <Label htmlFor={`plan-${value}`}>{label}</Label>
            <p className="text-muted-foreground text-xs">{desc}</p>
          </div>
        </div>
      ))}
    </RadioGroup>
  )
}

芯片

Select Shoe Size
tsx
import { useId } from "react"
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
export function RadioGroupChip() {
  const id = useId()
  const items = [
    { value: "1", label: "Size: 6 (UK)" },
    { value: "2", label: "Size: 7 (UK)", disabled: true },
    { value: "3", label: "Size: 8 (UK)" },
    { value: "4", label: "Size: 9 (UK)" },
    { value: "5", label: "Size: 10 (UK)" },
  ]
  return (
    <fieldset className="w-full max-w-96 space-y-4">
      <legend className="text-foreground text-sm leading-none font-medium">Select Shoe Size</legend>
      <RadioGroup className="grid grid-cols-3 gap-2" defaultValue="1">
        {items.map((item) => (
          <label
            key={`${id}-${item.value}`}
            className="border-input has-data-checked:border-primary/80 has-focus-visible:border-ring has-focus-visible:ring-ring/50 relative flex flex-col items-center gap-3 rounded-md border px-2 py-3 text-center shadow-xs transition-[color,box-shadow] outline-none has-focus-visible:ring-[3px] has-data-disabled:cursor-not-allowed has-data-disabled:opacity-50"
          >
            <RadioGroupItem
              id={`${id}-${item.value}`}
              value={item.value}
              className="sr-only after:absolute after:inset-0"
              aria-label={`size-radio-${item.value}`}
              disabled={item.disabled}
            />
            <p className="text-foreground text-sm leading-none font-medium">{item.label}</p>
          </label>
        ))}
      </RadioGroup>
    </fieldset>
  )
}

列表组

$39/mo
$69/mo
Custom
tsx
import { useId } from "react"
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
import { Badge } from "@polyui/react/badge"
export function RadioGroupListGroup() {
  const id = useId()
  const items = [
    { value: "1", label: "Pro", price: "$39/mo" },
    { value: "2", label: "Team", price: "$69/mo" },
    { value: "3", label: "Enterprise", price: "Custom" },
  ]
  return (
    <RadioGroup className="w-full max-w-96 gap-0 -space-y-px rounded-md shadow-xs" defaultValue="2">
      {items.map((item) => (
        <div
          key={`${id}-${item.value}`}
          className="border-input has-data-checked:border-primary/50 has-data-checked:bg-accent relative flex flex-col gap-4 border p-4 outline-none first:rounded-t-md last:rounded-b-md has-data-checked:z-10"
        >
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-2">
              <RadioGroupItem
                id={`${id}-${item.value}`}
                value={item.value}
                className="after:absolute after:inset-0"
                aria-label={`plan-radio-${item.value}`}
              />
              <Label className="inline-flex items-center gap-1.5" htmlFor={`${id}-${item.value}`}>
                {item.label}
                {item.value === "2" && <Badge className="rounded-sm px-1.5 py-px text-xs">Best Seller</Badge>}
              </Label>
            </div>
            <div className="text-muted-foreground text-xs">{item.price}</div>
          </div>
        </div>
      ))}
    </RadioGroup>
  )
}

拆分列表组

$39/mo
$69/mo
Custom
tsx
import { useId } from "react"
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
import { Badge } from "@polyui/react/badge"
export function RadioGroupSplitListGroup() {
  const id = useId()
  const items = [
    { value: "1", label: "Pro", price: "$39/mo" },
    { value: "2", label: "Team", price: "$69/mo" },
    { value: "3", label: "Enterprise", price: "Custom" },
  ]
  return (
    <RadioGroup className="w-full max-w-96 gap-0 space-y-2 rounded-md *:rounded-full" defaultValue="2">
      {items.map((item) => (
        <div
          key={`${id}-${item.value}`}
          className="border-input has-data-checked:bg-primary has-data-checked:text-primary-foreground relative flex flex-col gap-4 border p-4 outline-none has-data-checked:z-10"
        >
          <div className="group flex items-center justify-between">
            <div className="flex items-center gap-2">
              <RadioGroupItem
                id={`${id}-${item.value}`}
                value={item.value}
                aria-label={`plan-radio-${item.value}`}
                className="text-primary bg-accent data-checked:bg-primary-foreground! data-checked:border-primary-foreground after:absolute after:inset-0"
              />
              <Label className="inline-flex items-center gap-1.5" htmlFor={`${id}-${item.value}`}>
                {item.label}
                {item.value === "2" && (
                  <Badge
                    variant="outline"
                    className="rounded-sm border-green-500 bg-green-500/10 px-1.5 py-px text-xs text-green-500"
                  >
                    Best Seller
                  </Badge>
                )}
              </Label>
            </div>
            <div className="group-has-checked:text-primary-foreground text-xs">{item.price}</div>
          </div>
        </div>
      ))}
    </RadioGroup>
  )
}

卡片单选

Essential features for personal use.

Advanced features for power users.

tsx
import { useId } from "react"
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
export function RadioGroupCardRadio() {
  const id = useId()
  const cards = [
    {
      value: "1",
      label: "Basic",
      price: "Free",
      desc: "Essential features for personal use.",
    },
    {
      value: "2",
      label: "Premium",
      price: "$5.00",
      desc: "Advanced features for power users.",
    },
  ]
  return (
    <RadioGroup className="w-full max-w-96 gap-2" defaultValue="1">
      {cards.map((item) => (
        <div
          key={item.value}
          className="border-input has-data-checked:border-primary/50 relative flex w-full items-center gap-2 rounded-md border p-4 shadow-xs outline-none"
        >
          <RadioGroupItem
            value={item.value}
            id={`${id}-${item.value}`}
            aria-label={`plan-radio-${item.label.toLowerCase()}`}
            className="size-5 after:absolute after:inset-0 [&_svg]:size-3"
          />
          <div className="grid grow gap-2">
            <Label htmlFor={`${id}-${item.value}`} className="justify-between">
              {item.label}{" "}
              <span className="text-muted-foreground text-xs leading-[inherit] font-normal">{item.price}</span>
            </Label>
            <p className="text-muted-foreground text-xs">{item.desc}</p>
          </div>
        </div>
      ))}
    </RadioGroup>
  )
}

带边框卡片单选

tsx
import { useId } from "react"
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
export function RadioGroupCardRadioWithBorder() {
  const id = useId()
  const cards = [
    {
      value: "1",
      label: "Basic",
      price: "Free",
      desc: "Essential features for personal use.",
    },
    {
      value: "2",
      label: "Premium",
      price: "$5.00",
      desc: "Advanced features for power users.",
    },
  ]
  return (
    <RadioGroup className="w-full max-w-96 gap-2" defaultValue="1">
      {cards.map((item) => (
        <div
          key={item.value}
          className="border-input has-data-checked:border-primary/50 has-focus-visible:border-ring has-focus-visible:ring-ring/50 relative w-full rounded-md border p-3 shadow-xs transition-[color,box-shadow] outline-none has-focus-visible:ring-[3px]"
        >
          <RadioGroupItem
            value={item.value}
            id={`${id}-${item.value}`}
            className="sr-only"
            aria-label={`plan-radio-${item.label.toLowerCase()}`}
          />
          <Label
            htmlFor={`${id}-${item.value}`}
            className="text-foreground flex flex-col items-start after:absolute after:inset-0"
          >
            <div className="flex w-full items-center justify-between">
              <span>{item.label}</span>
              <span className="text-muted-foreground text-xs leading-[inherit] font-normal">{item.price}</span>
            </div>
            <p className="text-muted-foreground text-xs">{item.desc}</p>
          </Label>
        </div>
      ))}
    </RadioGroup>
  )
}

垂直卡片

Essential features for personal use.

Advanced features for power users.

tsx
import { useId } from "react"
import { RadioGroup, RadioGroupItem } from "@polyui/react/radio-group"
import { Label } from "@polyui/react/label"
import { UserIcon, CrownIcon } from "lucide-react"
export function RadioGroupCardVertical() {
  const id = useId()
  const plans = [
    {
      value: "1",
      label: "Basic",
      desc: "Essential features for personal use.",
      Icon: UserIcon,
    },
    {
      value: "2",
      label: "Premium",
      desc: "Advanced features for power users.",
      Icon: CrownIcon,
    },
  ]
  return (
    <RadioGroup className="w-full max-w-96 justify-items-center sm:grid-cols-2" defaultValue="1">
      {plans.map(({ value, label, desc, Icon }) => (
        <div
          key={value}
          className="border-input has-data-checked:border-primary/50 relative flex w-full max-w-50 flex-col items-center gap-3 rounded-md border p-4 shadow-xs outline-none"
        >
          <RadioGroupItem
            value={value}
            id={`${id}-${value}`}
            className="order-1 size-5 after:absolute after:inset-0 [&_svg]:size-3"
            aria-label={`plan-radio-${label.toLowerCase()}`}
          />
          <div className="grid grow justify-items-center gap-2">
            <Icon />
            <Label htmlFor={`${id}-${value}`} className="justify-center">
              {label}
            </Label>
            <p className="text-muted-foreground text-center text-xs">{desc}</p>
          </div>
        </div>
      ))}
    </RadioGroup>
  )
}

Props — RadioGroup

属性类型默认值说明
valuestring受控模式下当前选中的值
defaultValuestring非受控模式下的默认选中值
onValueChange(value: string) => void选中值变化时的回调
disabledbooleanfalse是否禁用整个单选组
classNamestring自定义样式类名

Props — RadioGroupItem

属性类型默认值说明
valuestring该选项的值,必填
disabledbooleanfalse是否禁用该选项
classNamestring自定义样式类名