Radio Group
A set of mutually exclusive options - each item carries its own tone and size, wired to radix keyboard navigation.
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
export default function RadioGroupDemo() {
return (
<RadioGroup defaultValue="weekly">
<Label>
<RadioGroupItem value="realtime" tone="brand" /> Realtime alerts
</Label>
<Label>
<RadioGroupItem value="weekly" tone="brand" /> Weekly digest
</Label>
</RadioGroup>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/radio-groupDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install class-variance-authority@^0.7.1 radix-ui@^1.6.1Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/radio-group.tsx"use client"
import type * as React from "react"
import { RadioGroup as RadioGroupPrimitive } from "@/components/ui/primitives"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
)
}
const radioItemVariants = cva(
"relative aspect-square shrink-0 rounded-full border border-field-border shadow-xs outline-none transition-colors duration-(--motion-fast) after:absolute after:-inset-1.5 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-danger aria-invalid:ring-danger/20 dark:aria-invalid:ring-danger/40 dark:bg-input/30",
{
variants: {
tone: {
neutral: "data-[state=checked]:border-primary text-primary",
brand: "data-[state=checked]:border-brand text-brand",
success: "data-[state=checked]:border-success text-success",
danger: "data-[state=checked]:border-danger text-danger",
},
size: {
sm: "size-3.5",
md: "size-4",
lg: "size-5",
},
},
defaultVariants: { tone: "neutral", size: "md" },
}
)
interface RadioGroupItemProps
extends React.ComponentProps<typeof RadioGroupPrimitive.Item>,
VariantProps<typeof radioItemVariants> {}
function RadioGroupItem({ className, tone, size, ...props }: RadioGroupItemProps) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
data-tone={tone ?? "neutral"}
className={cn(radioItemVariants({ tone, size, className }))}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="flex size-full items-center justify-center"
>
<span className="size-1/2 rounded-full bg-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem, radioItemVariants, type RadioGroupItemProps }Manual installs skip the @ai2/tokens theme - add the token CSS from the theming guide or the tone colors will be missing.
Usage
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Label } from "@/components/ui/label"
<RadioGroup defaultValue="weekly">
<Label>
<RadioGroupItem value="realtime" tone="brand" /> Realtime alerts
</Label>
<Label>
<RadioGroupItem value="weekly" tone="brand" /> Weekly digest
</Label>
</RadioGroup>Wrap each item in a Label to get a full-row click target. tone and size are optional - omit them for the neutral medium radio.
Examples
Tones
The tone colors the checked border and the indicator dot only - unchecked items stay on the neutral input border. Each radio above sits in its own group so every tone renders checked.
Sizes
The indicator dot renders at half the control size, so it scales with the size axis automatically.
Disabled item
Invalid
Set aria-invalid on the items when a required choice is missing. The danger border and ring style is built in and matches every other ai2 form control.
Props
The table below covers RadioGroupItem. Both parts also accept every radix prop - defaultValue, value, onValueChange and disabled included.
| Prop | Type | Default | Description |
|---|---|---|---|
tone | "neutral" | "brand" | "success" | "danger" | "neutral" | Semantic color of the checked border and indicator dot, mapped to theme tokens. |
size | "sm" | "md" | "lg" | "md" | Diameter of the radio control. |
ai2 Radio Group: mutually exclusive options for React
The ai2 Radio Group is a shadcn-compatible radio group component for React, built on the radix-ui RadioGroup primitive and styled with Tailwind CSS v4. It renders a set of mutually exclusive options where each item carries its own tone and size, with roving focus keyboard navigation handled entirely by the primitive.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Radio Group?
It is a two-part composition: RadioGroup lays the options out on a grid, and each RadioGroupItem renders the circular control with an indicator dot inside. The anatomy matches shadcn/ui exactly, so existing snippets, muscle memory and AI agents keep working without changes.
Each item takes two cva axes: tone (neutral, brand, success, danger) colors the checked border and dot, and size (sm, md, lg) sets the control diameter. The dot uses the current text color at half the control size, so it always matches the tone automatically.
Why use it
- Accessible by construction: The radix-ui RadioGroup primitive implements the WAI-ARIA radio group pattern: proper roles, checked state and focus management come for free.
- Roving focus keyboard navigation: The group is a single tab stop. Tab lands on the checked item, arrow keys move focus and select in one gesture, and Space checks the focused item.
- Tones per item: Four tones map to theme tokens, and because tone lives on the item, a single group can mix a danger-toned destructive option with neutral ones.
- Calm unchecked state: Tone only colors the checked border and the indicator dot; unchecked items keep the neutral input border, so a toned group never shouts before selection.
- Agent-readable metadata: The registry item describes its tone and size axes in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, its dependencies and the @ai2/tokens theme to your project.
- 4 tones x 3 sizes per item: Twelve combinations from two props on each item, resolved through cva with neutral and md as defaults.
- Current-color indicator dot: The dot renders with bg-current at half the control size, so it inherits the tone color and scales with the size axis without extra props.
- Focus ring and invalid styling: Items ship the ai2 focus-visible ring and switch to the danger border and ring under aria-invalid, so form validation states look right out of the box.
- Controlled and uncontrolled state: Use defaultValue for simple cases or drive value and onValueChange from React state when other UI depends on the selection.
- Data attributes for styling: Every part exposes data-slot, and items expose data-tone and data-state, so you can restyle checked and unchecked states from CSS without forking the component.
Production tips
- Wrap items in a Label: Putting RadioGroupItem inside an ai2 Label gives the whole row a click target and wires the accessible name in one move, as every example on this page does.
- Radios for short lists only: Radio groups shine at two to five always-visible options. For longer lists reach for the ai2 Select, which collapses the choices behind a trigger.
- Preselect a sensible default: A radio group cannot be unchecked by the user once a selection exists. Start with defaultValue on the safest option instead of an empty group that traps validation.
- Keep one tone per form group: Mixed tones are possible but read as noise in plain forms. Reserve per-item tones for genuinely semantic cases, like a danger tone on a destructive plan change.
- Explain disabled options: A grayed radio with no context frustrates users. Add a short reason in the label text, like Enterprise (contact sales) in the example above.
Works with the rest of ai2
The radio group composes naturally with the rest of the registry. Wrap each item in an ai2 Label for full-row clicks, or place the group inside an ai2 Field to pick up form-level labels, descriptions and error messages.
For neighboring inputs, use the ai2 Checkbox when options are independent rather than exclusive, the ai2 Select when the list grows past a handful, and an ai2 Card per option for plan pickers. Everything shares one token source, so combinations stay visually consistent in both modes.