Primitives
Radio group
Single-choice radio set for plans, filters, and option lists.
Composition
Anatomy
- <RadioGroup>
- <RadioGroupItem>
- <Radio />
- <RadioGroupItem>
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
The Control UI source installs this primitive from src/registry/sources/control-ui/ui/radio-group.tsx.
npx shadcn@latest add https://control-ui.dev/r/radio-group.jsonRaw code
Primary installed source
"use client";import { Radio as RadioPrimitive } from "@base-ui/react/radio";import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";import type { ComponentProps, CSSProperties } from "react";import type { ControlledChoice } from "@/components/control-ui/control-props";import type { ChoiceKnobStyle } from "@/components/control-ui/knob-contracts/choice-knobs";import { cn } from "@/components/control-ui/lib/cn";export type RadioGroupProps<TValue extends string = string> = Omit<ComponentProps<"div">, "defaultValue" | "onChange"> & ControlledChoice<TValue> & { disabled?: boolean; readOnly?: boolean; required?: boolean; name?: string; orientation?: "horizontal" | "vertical"; } & { style?: CSSProperties & ChoiceKnobStyle };export type RadioProps = Omit< { value: string; disabled?: boolean; readOnly?: boolean; required?: boolean; id?: string; className?: string; "aria-label"?: string; "aria-labelledby"?: string; "aria-describedby"?: string; "aria-invalid"?: boolean | "true" | "false"; }, "style"> & { style?: CSSProperties & ChoiceKnobStyle };// `orientation` is visual only — Base UI owns roving focus on both axes and exposes no such prop.export function RadioGroup<TValue extends string = string>({ className, orientation = "vertical", ...props }: RadioGroupProps<TValue>) { return ( <RadioGroupPrimitive<TValue> data-control-ui="radio-group" data-control-family="choice" data-choice-kind="radio-group" data-slot="root" data-orientation={orientation} className={cn("flex data-[orientation=horizontal]:flex-row data-[orientation=vertical]:flex-col", className)} {...props} /> );}export function Radio({ className, disabled, ...props }: RadioProps) { return ( <RadioPrimitive.Root data-control-ui="radio-group" data-choice-kind="radio-group" data-slot="item" data-control-family="choice" disabled={disabled} className={cn( "group/radio relative inline-flex shrink-0 cursor-pointer items-center justify-center", "data-[disabled]:cursor-not-allowed", className, )} {...props} > <RadioPrimitive.Indicator data-control-ui="radio-group" data-control-family="choice" data-choice-kind="radio-group" data-slot="indicator" className="data-[unchecked]:hidden" /> </RadioPrimitive.Root> );}export function RadioGroupItem({ className, htmlFor, children, ...props }: ComponentProps<"label">) { return ( <label htmlFor={htmlFor} data-control-ui="radio-group" data-slot="label" className={cn( "flex cursor-pointer items-start gap-2.5 rounded-(--radius-control) p-2 text-body has-[[data-disabled]]:cursor-not-allowed [&>[data-control-family=choice]]:mt-[calc((1lh_-_var(--spacing)*4)/2)]", className, )} {...props} > {children} </label> );}Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-choice-* · 7 knobsHow the cascade resolves--cui-choice-radius<length-percentage>var(--radius-sm)--cui-choice-border-color<color>var(--control-boundary)--cui-choice-border-width<length>var(--control-rim-width)--cui-choice-background<color>var(--control-fill)--cui-choice-checked-background<color>var(--primary)--cui-choice-checked-border-color<color>var(--primary)--cui-choice-shadow*inset 0 0 0 var(--cui-choice-border-width) var(--cui-choice-border-color), var(--shadow-sm)