V2 componentsUpdated Oct 15, 2026
Radio Group
Pick exactly one option from a short, always-visible set (2-6 options). One tab stop for the group; arrow keys move and select. Put the group in a Field and each option in a Field.Item with a Field.Label (and an optional Field.Note), the same label, note and disabled treatment as Checkbox; no separate label component. For a long list use Select; for independent on/off choices use Checkbox. A 16px blue dot control, with the invalid, disabled and focus states built in.
Source: packages/ui/src/components/radio-group
import { RadioGroup } from '@highlight/ui'Change the props; the preview and the snippet update together.
<Field> <RadioGroup defaultValue="Summary only" aria-label="Meeting recap"> <Field.Item> <RadioGroup.Item value="Summary only" /> <Field.Label>Summary only</Field.Label> </Field.Item> <Field.Item> <RadioGroup.Item value="Summary and transcript" /> <Field.Label>Summary and transcript</Field.Label> </Field.Item> <Field.Item> <RadioGroup.Item value="No recap" /> <Field.Label>No recap</Field.Label> </Field.Item> </RadioGroup></Field>Uncontrolled with defaultValue. Field.Item disabled dims its radio, label and note together.
Sent once the transcript is ready.
Managed by your organization.
import { Field, RadioGroup } from '@highlight/ui' export default function RadioGroupBasic() { return ( <Field className="max-w-sm"> <RadioGroup defaultValue="summary" aria-label="Meeting recap"> <Field.Item> <RadioGroup.Item value="summary" /> <Field.Label>Summary only</Field.Label> </Field.Item> <Field.Item> <RadioGroup.Item value="full" /> <Field.Label>Summary and transcript</Field.Label> <Field.Note>Sent once the transcript is ready.</Field.Note> </Field.Item> <Field.Item disabled> <RadioGroup.Item value="none" /> <Field.Label>No recap</Field.Label> <Field.Note>Managed by your organization.</Field.Note> </Field.Item> </RadioGroup> </Field> )}value and onValueChange, the same shape as Base UI.
Sync to: Slack
import { useState } from 'react'import { Field, RadioGroup } from '@highlight/ui' const OPTIONS = ['Slack', 'Linear', 'Notion'] as const export default function RadioGroupControlled() { const [value, setValue] = useState<string>('Slack') return ( <div className="flex flex-col gap-3"> <Field> <RadioGroup value={value} onValueChange={(next) => setValue(String(next))} aria-label="Sync destination"> {OPTIONS.map((name) => ( <Field.Item key={name}> <RadioGroup.Item value={name} /> <Field.Label>{name}</Field.Label> </Field.Item> ))} </RadioGroup> </Field> <p className="text-foreground-secondary text-xs">Sync to: {value}</p> </div> )}Field invalid turns every radio's edge and focus ring to danger; Field.Note error says why.
import { Field, RadioGroup } from '@highlight/ui' export default function RadioGroupInvalid() { return ( <Field invalid> <RadioGroup aria-label="Plan" required> <Field.Item> <RadioGroup.Item value="team" /> <Field.Label>Team</Field.Label> </Field.Item> <Field.Item> <RadioGroup.Item value="enterprise" /> <Field.Label>Enterprise</Field.Label> </Field.Item> </RadioGroup> <Field.Note error match> Pick a plan to continue. </Field.Note> </Field> )}The root, then any compound sub-components. Every part accepts className and forwards the rest of its props to the element or Base UI part beneath it.
RadioGroup- Base UI RadioGroup. Owns the selected value; holds the items.
RadioGroup.Item- One radio. Needs a value. Renders the dot by default; pass children to replace it.
RadioGroup.Indicator- The dot; scales in when checked. Only needed inside a custom Item.
- Root props pass through from Base UI RadioGroup: name, value, defaultValue, onValueChange, disabled, readOnly, required.
- Always give the group an accessible name: aria-label, or aria-labelledby pointing at a heading.