Radio group
StableA fieldset of native radios, each a ring with the brand square inside when chosen, with optional hints.
For choosing one of a few options that people should see side by side. It renders a <fieldset> with a <legend> and one native radio per option, so the browser handles arrow keys and the single Tab stop. Options can carry a hint, and a disabled option stays visible.
With hints
import { useState } from 'react'import { RadioGroup } from '@rchursin/product/ui'
type Method = 'passkey' | 'app' | 'email' | 'sms'
/** * Native radios in a fieldset: Tab enters the group at the chosen option, arrow keys move and * choose. A disabled option stays visible and its hint says why. */export default function RadioGroupSignIn() { const [method, setMethod] = useState<Method>('passkey')
return ( <RadioGroup<Method> legend="Second step at sign-in" name="method" className="w-full max-w-sm" value={method} onChange={setMethod} options={[ { value: 'passkey', label: 'Passkey', hint: 'Fingerprint, face or screen lock.' }, { value: 'app', label: 'Authenticator app', hint: 'A 6-digit code that changes every 30 seconds.' }, { value: 'email', label: 'Code by email' }, { value: 'sms', label: 'Code by text message', hint: 'Not offered: texts can be intercepted.', disabled: true }, ]} /> )}Usage
import { RadioGroup } from '@rchursin/product/ui'<RadioGroup legend="Export tasks as" value={format} onChange={setFormat} options={[ { value: 'csv', label: 'CSV, for spreadsheets' }, { value: 'json', label: 'JSON, for scripts' }, ]}/>Examples
Short labels
No hints needed. The button repeats the choice, so the result is never a surprise.
import { useState } from 'react'import { Button, RadioGroup } from '@rchursin/product/ui'
type Format = 'csv' | 'json' | 'markdown'
const names: Record<Format, string> = { csv: 'CSV', json: 'JSON', markdown: 'Markdown' }
/** Short labels need no hints. The button repeats the choice, so the result is never a surprise. */export default function RadioGroupExport() { const [format, setFormat] = useState<Format>('csv')
return ( <div className="flex w-full max-w-sm flex-col gap-5"> <RadioGroup<Format> legend="Export tasks as" value={format} onChange={setFormat} options={[ { value: 'csv', label: 'CSV, for spreadsheets' }, { value: 'json', label: 'JSON, for scripts' }, { value: 'markdown', label: 'Markdown, for notes' }, ]} /> <Button variant="secondary" icon="download" className="self-start"> Export {names[format]} </Button> </div> )}API
Radio group props
| Prop | Type | Default | Description |
|---|---|---|---|
legendrequired | ReactNode | – | The question, rendered as the fieldset’s <legend>. |
optionsrequired | { value: V; label: ReactNode; hint?: ReactNode; disabled?: boolean }[] | – | One radio each. A hint goes under its label and is linked with aria-describedby. |
valuerequired | V | – | The chosen option’s value. V is your string union, inferred from options. |
onChangerequired | (v: V) => void | – | Called with the new value. |
name | string | useId() | The radios’ shared name, sent with a form. Each radio’s id comes from useId, so two groups may share a name without clashing. |
className | string | – | Classes on the <fieldset>. |
CSS
| Class or variable | What it does |
|---|---|
rc-radio | On <input type="radio">: a 16px ring; when :checked the ring firms up and a 5px square appears in the middle. Flips in dark mode. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves into the group, onto the chosen radio, and out again. The group is one Tab stop. |
| Down arrowRight arrow | Moves to the next option and chooses it, skipping disabled ones; wraps from the last to the first. |
| Up arrowLeft arrow | Moves to the previous option and chooses it. |
| Space | Chooses the focused option, when none was chosen yet. |
- Native radios in a <fieldset>: screen readers announce the legend, then “radio button, 2 of 4”.
- Each label is a <label for>, so the text is a click target as large as the words.
- The chosen state is a shape (the square inside the ring), not a colour.
Guidelines
Do
- Use it for 2 to 6 options that people should compare.
- Choose a sensible default when there is one.
- Explain a disabled option in its hint.
Don’t
- Don’t use radios for choices that can combine: use checkboxes.
- Don’t use them for long lists: use Select or Combobox.
In use
Related
- Radio cardsA radio group drawn as cards, for choices that each need a sentence, where the whole card is the target.
- Segmented controlA radio group drawn as one strip, with a lit pill that glides to the choice and arrow keys that move and choose.
- SelectThe browser’s own select in the field finish, with an up-down chevron, so phones show their own picker and the keys work for free.
- CheckboxA labelled checkbox drawn as the brand square with a cut-in tick, with a hint and a mixed state.
Source
The component’s own files, exactly as they run on this site.
/** * Form fields. Labels sit above inputs, hints under the label, errors between the hint and the * input with an icon and the word "Error:" for screen readers (never colour alone). * Optional fields say "(optional)"; required ones carry no mark. * * <Field label="Email" hint="We send the sign-in code here." error={err}> * {(p) => <TextInput {...p} type="email" autoComplete="email" />} * </Field> */import { forwardRef, useId, useLayoutEffect, useRef, useState, type InputHTMLAttributes, type KeyboardEvent, type ReactNode, type SelectHTMLAttributes, type TextareaHTMLAttributes,