Single-choice radios with optional full-card labels, descriptions, and large pointer targets around Radix keyboard navigation.
Digest frequency
Choose when to get your project summary.
"use client";
import * as React from "react";
import { RadioGroup, RadioGroupOption } from "@/components/ballmac/radio-group";
export default function RadioGroupDemo() {
const [value, setValue] = React.useState("weekly");
return (
<div className="w-full max-w-sm">
<p className="mb-1 text-sm font-semibold">Digest frequency</p>
<p className="mb-4 text-sm text-muted-foreground">
Choose when to get your project summary.
</p>
<RadioGroup
aria-label="Digest frequency"
value={value}
onValueChange={setValue}
>
<RadioGroupOption
value="daily"
title="Daily"
description="A brief update every morning"
/>
<RadioGroupOption
value="weekly"
title="Weekly"
description="A focused summary each Monday"
/>
<RadioGroupOption
value="off"
title="Off"
description="Only important account notices"
/>
</RadioGroup>
</div>
);
}Installation
$ pnpm dlx shadcn@latest add @ballmac/radio-groupInstall the dependencies.
$ pnpm add radix-ui lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/directionCopy the source into your project.
components/ballmac/radio-group.tsx// Ballmac UI: Radio Group. https://ui.ballmac.com/components/radio-group "use client"; import * as React from "react"; import { Circle } from "lucide-react"; import { RadioGroup as Primitive } from "radix-ui"; import { useDirection } from "@/lib/ballmac/direction"; import { cn } from "@/lib/utils"; type RadioGroupProps = React.ComponentProps<typeof Primitive.Root>; function RadioGroup({ className, ...props }: RadioGroupProps) { const dir = useDirection(props.dir); return ( <Primitive.Root dir={dir} data-slot="radio-group" className={cn("grid gap-2", className)} {...props} /> ); } type RadioGroupItemProps = React.ComponentProps<typeof Primitive.Item>; function RadioGroupItem({ className, ...props }: RadioGroupItemProps) { return ( <Primitive.Item data-slot="radio-group-item" className={cn( "aspect-square size-4 shrink-0 rounded-full border border-input bg-background text-primary shadow-xs outline-none transition-[color,border-color,box-shadow] duration-150 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary motion-reduce:transition-none", className, )} {...props} > <Primitive.Indicator data-slot="radio-group-indicator" className="flex items-center justify-center" > <Circle aria-hidden="true" className="size-2 fill-current" /> </Primitive.Indicator> </Primitive.Item> ); } type RadioGroupOptionProps = React.ComponentProps<"label"> & { /** Heading shown beside the radio control. */ title: string; /** Supporting copy below the option heading. */ description?: string; /** Value submitted by the radio group. */ value: string; /** Disables this choice. */ disabled?: boolean; }; function RadioGroupOption({ className, title, description, value, disabled, children, ...props }: RadioGroupOptionProps) { return ( <label data-slot="radio-group-option" data-disabled={disabled || undefined} className={cn( "flex min-h-14 cursor-pointer items-start gap-3 rounded-lg border bg-card p-3 text-card-foreground shadow-xs transition-[border-color,background-color,box-shadow] duration-150 hover:bg-accent/50 has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary/5 has-[:focus-visible]:ring-[3px] has-[:focus-visible]:ring-ring/50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50 motion-reduce:transition-none", className, )} {...props} > <RadioGroupItem value={value} disabled={disabled} className="mt-0.5" /> <span className="grid min-w-0 flex-1 gap-0.5"> <span className="text-sm font-medium">{title}</span> {description && ( <span className="text-sm leading-snug text-muted-foreground"> {description} </span> )} </span> {children} </label> ); } export { RadioGroup, RadioGroupItem, RadioGroupOption, type RadioGroupProps, type RadioGroupItemProps, type RadioGroupOptionProps, };Update the import paths to match your project setup.
Usage
import { RadioGroup, RadioGroupItem, RadioGroupOption } from "@/components/ballmac/radio-group"The full example is in the Code tab above.
Examples
Compact choices
Export format
import { RadioGroup, RadioGroupItem } from "@/components/ballmac/radio-group";
export default function RadioGroupStates() {
return (
<div className="w-full max-w-xs">
<p className="mb-3 text-sm font-medium">Export format</p>
<RadioGroup aria-label="Export format" defaultValue="pdf">
{[
{ value: "pdf", label: "PDF document" },
{ value: "csv", label: "CSV spreadsheet" },
{ value: "json", label: "JSON data" },
].map((option) => (
<label
key={option.value}
className="flex min-h-9 items-center gap-3 text-sm"
>
<RadioGroupItem value={option.value} />
{option.label}
</label>
))}
</RadioGroup>
</div>
);
}API reference
| Prop | Type | Default |
|---|---|---|
title*Heading shown beside the radio control. | string | — |
descriptionSupporting copy below the option heading. | string | — |
value*Value submitted by the radio group. | string | — |
disabledDisables this choice. | boolean | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab | Enters the radio group |
| Arrow keys | Moves and selects among enabled choices |
| Space | Selects the focused choice |
Use with AI
Choose exactly one option in a form; use descriptive card labels when choices need context. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Radio Group (@ballmac/radio-group) to this project with the shadcn MCP, then use it where it fits.
Use it for
- A few mutually exclusive preferences
- Pricing or delivery options with explanations
Not for
- Many options; use select
- Multiple selections; use checkbox or toggle-group
Registry JSON: https://ui.ballmac.com/r/radio-group.json
Credits
Free to use in personal and commercial projects.
- Registry
- @ballmac/directionshadcn/utils
Pairs well with
Label
An accessible form label built on Radix Label that dims itself when the paired peer control is disabled and never selects text on double-click.
Calendar
A date grid for one day, several days or a range, with month and year selects, week numbers, disabled rules and range-end styling, on React DayPicker.
Color Picker
A native color chooser paired with an editable hex field and a live swatch.
Combobox
A searchable single-choice select with groups, descriptions, keywords, clearable value, invalid state and hidden-input form submission.