Radio
A styled native radio input, plus two higher-level building blocks — a card-style RadioArea and a labeled RadioInput — for building single-choice groups.
Overview
The Radio family gives you three layers, from bare input to fully labeled controls:
Radio— a styled native<input type="radio">with size variants and nothing else. Use it when you're building a fully custom layout around the radio yourself (e.g. inside a design that doesn't matchRadioAreaorRadioInput).RadioArea— a card-style choice built on top ofRadio. The whole card is clickable, and it supports a label, hint, and optional leading/trailing elements (startEl/endEl). Use it for card-based choice groups (plans, payment methods, layout options, …).RadioInput— a form-style labeled radio built on top ofRadio, integrated withInputLabelandInputStatus. Use it for standard form radio groups where you want an inline label, hint text, and validation status.
All three are independent public exports and are typically used as a group sharing the same name, with checked/onChange driven from a single piece of state.
Import
import { Radio } from '@unflow.io/ui/components/Radio';
import { RadioArea } from '@unflow.io/ui/components/RadioArea';
import { RadioInput } from '@unflow.io/ui/components/RadioInput';Radio
The bare styled radio input. All standard HTMLInputElement props (checked, value, name, onChange, onBlur, required, disabled, ...) are forwarded to the underlying <input>.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'xs' | 'sm' | 'md' | 'lg' | 'xs' | Controls the diameter of the radio |
checked | boolean | — | Controlled checked state |
value | string | — | Value submitted/reported when this option is selected |
name | string | — | Groups radios together — only one per shared name can be checked |
onChange | ChangeEventHandler<HTMLInputElement> | — | Change handler |
onBlur | FocusEventHandler<HTMLInputElement> | — | Blur handler |
disabled | boolean | — | Disables the input |
required | boolean | — | Marks the input as required |
className | string | — | Applied directly to the <input> element |
A console warning fires in development if aria-label is missing, so always pair Radio with a visible label (e.g. via <label>) or pass aria-label explicitly.
Basic group
'use client';
import { useState } from 'react';
import { Radio } from '@unflow.io/ui/components/Radio';
export function Example() {
const [value, setValue] = useState('option_1');
return (
<>
{['option_1', 'option_2', 'option_3'].map((option) => (
<label key={option} className="flex items-center gap-2">
<Radio
name="basic_group"
value={option}
checked={value === option}
onChange={(e) => setValue(e.target.value)}
/>
<span>{option}</span>
</label>
))}
</>
);
}RadioArea
Props
RadioArea accepts every Radio prop (minus size's effect on layout, which still applies to the hidden input) plus:
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | undefined | — | Required. Card label. Pass undefined explicitly if you render fully custom children instead |
hint | string | — | Helper text shown under the label |
variant | 'outlined' | 'solid' | 'outlined' | Visual style of the card |
fullWidth | boolean | — | Makes the card span the width of its container |
startEl | ReactNode | — | Element rendered before the label content (e.g. an icon) |
endEl | ReactNode | — | Element rendered after the label content |
children | ReactNode | — | Fully custom content — when provided, it replaces the built-in label/hint block |
disabled | boolean | — | Disables interaction and dims the card |
Customization (slots & slotProps)
RadioArea has no overridable slots, only slotProps.
| Slot Prop | Props | Description |
|---|---|---|
labelRoot | BaseHTMLProps<HTMLLabelElement> | The outer <label> element that wraps the whole card |
contentRoot | BaseHTMLProps<HTMLDivElement> | Wrapper around the label/hint text block |
label | BaseHTMLProps<HTMLParagraphElement> | The label <p> element |
hint | BaseHTMLProps<HTMLParagraphElement> | The hint <p> element |
Card group
'use client';
import { useState } from 'react';
import { RadioArea } from '@unflow.io/ui/components/RadioArea';
export function Example() {
const [value, setValue] = useState('option_1');
return (
<>
{['option_1', 'option_2', 'option_3'].map((option, idx) => (
<RadioArea
key={option}
name="plan"
label={`Option ${idx + 1}`}
hint="This is a hint text to help user."
value={option}
fullWidth
checked={value === option}
onChange={(e) => setValue(e.target.value)}
/>
))}
</>
);
}Solid variant with start/end elements
<RadioArea
name="plan"
variant="solid"
label="Pro plan"
hint="Best for growing teams."
value="pro"
fullWidth
startEl={<CloudFog size={18} />}
checked={value === 'pro'}
onChange={(e) => setValue(e.target.value)}
/>RadioInput
Props
RadioInput accepts every Radio prop plus:
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Visible label rendered via InputLabel |
hint | string | — | Helper text. Shown next to the radio when inline, otherwise below it |
inline | boolean | — | Lays the label and radio out on one line instead of stacking |
placement | 'start' | 'end' | 'start' | Whether the radio sits before or after the label content |
status | 'valid' | 'invalid' | 'error' | — | Validation status, reflected in styling and (when not inline) an InputStatus indicator |
statusMessage | string | — | Message shown by the InputStatus indicator |
fullWidth | boolean | — | Makes the control span the width of its container |
disabled | boolean | — | Disables the input and dims the label |
required | boolean | — | Marks the input as required (shows the required symbol on the label) |
A console warning fires in development if none of aria-label, label, or hint is provided.
Customization (slots & slotProps)
RadioInput has no overridable slots, only slotProps.
| Slot Prop | Props | Description |
|---|---|---|
inputContainer | BaseHTMLProps<HTMLDivElement> | Wrapper around the radio, hint, and status indicator |
inputLabel | Omit<IInputLabel, 'label' | 'hint' | 'required' | 'fullWidth' | 'children' | 'inline' | 'status' | 'statusMessage'> | The outer InputLabel component |
hint | BaseHTMLProps<HTMLParagraphElement> | The hint <p> element (non-inline mode) |
status | Omit<IInputStatus, 'status' | 'message'> | The InputStatus indicator (non-inline mode) |
Form group
'use client';
import { useState } from 'react';
import { RadioInput } from '@unflow.io/ui/components/RadioInput';
export function Example() {
const [value, setValue] = useState('option_1');
return (
<>
{['option_1', 'option_2', 'option_3'].map((option, idx) => (
<RadioInput
key={option}
name="radio_group"
label={`Option ${idx + 1}`}
hint="This is a hint text to help user."
value={option}
inline
checked={value === option}
onChange={(e) => setValue(e.target.value)}
/>
))}
</>
);
}With validation status
<RadioInput
name="radio_group"
label="Option with an error"
status="error"
statusMessage="This option is currently unavailable."
value="option_1"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>Accessibility
Radiowarns in development if rendered without anaria-label— always pair it with a visible label (RadioArea/RadioInputhandle this for you) or passaria-labelexplicitly.RadioAreawraps the hidden radio input in a<label>, so clicking anywhere on the card toggles the option; disabled cards useinertto remove them from the accessibility tree and tab order.RadioInputrenders its label throughInputLabel, keeping the visible label programmatically associated with the input, and surfaces validation state throughInputStatus.- As with any native radio group, give every option in the group the same
nameso screen readers and keyboard arrow navigation treat them as a single group.