Your three options
| Option | Install | Expo Go | Best for |
|---|---|---|---|
| Build with Pressable | Nothing | Yes | Custom look, one or two groups, full control |
| React Native Paper RadioButton | react-native-paper | Yes | Apps already using Paper / Material Design |
| react-native-radio-buttons-group | Small JS package | Yes | Quick prototypes with default styling |
All three are plain JavaScript on top of core components, so none needs a development build. If your app already uses React Native Paper, use its RadioButton.Groupand stop reading. Otherwise, building it yourself is less code than learning a library's styling API, and you get a control that looks like the rest of your app.
One value for the group, not one boolean per option
The defining rule of a radio group is that exactly one option is selected. Model that directly: the group owns one value, and each option is selected when its value equals it.
// Fragile: nothing stops two of these being true
const [monthly, setMonthly] = useState(true);
const [yearly, setYearly] = useState(false);
// Robust: one value, impossible to select two
type Plan = 'monthly' | 'yearly';
const [plan, setPlan] = useState<Plan>('yearly');The single value is also what you save to your database or send to your payment screen, so there is no translation step. A union type like Plan means TypeScript catches a typo in an option value before it ships.
The component: accessible radio group in about 40 lines
import { Pressable, StyleSheet, Text, View } from 'react-native';
type Option<T extends string> = { value: T; label: string; hint?: string };
export function RadioGroup<T extends string>({
label,
options,
value,
onChange,
}: {
label: string;
options: Option<T>[];
value: T;
onChange: (next: T) => void;
}) {
return (
<View accessibilityRole="radiogroup" accessibilityLabel={label}>
{options.map((opt) => {
const checked = opt.value === value;
return (
<Pressable
key={opt.value}
onPress={() => onChange(opt.value)}
accessibilityRole="radio"
accessibilityState={{ checked }}
accessibilityLabel={opt.label}
accessibilityHint={opt.hint}
style={styles.row}
>
<View style={[styles.circle, checked && styles.circleOn]}>
{checked ? <View style={styles.dot} /> : null}
</View>
<View style={{ flex: 1 }}>
<Text style={styles.label}>{opt.label}</Text>
{opt.hint ? <Text style={styles.hint}>{opt.hint}</Text> : null}
</View>
</Pressable>
);
})}
</View>
);
}
const styles = StyleSheet.create({
row: { flexDirection: 'row', alignItems: 'center', minHeight: 48, paddingVertical: 10, gap: 12 },
circle: { width: 22, height: 22, borderRadius: 11, borderWidth: 2, borderColor: '#6b6b6b',
alignItems: 'center', justifyContent: 'center' },
circleOn: { borderColor: '#fb923c' },
dot: { width: 10, height: 10, borderRadius: 5, backgroundColor: '#fb923c' },
label: { fontSize: 16, color: '#fff' },
hint: { fontSize: 13, color: '#8a8a8a', marginTop: 2 },
});Using it:
const [plan, setPlan] = useState<'monthly' | 'yearly'>('yearly');
<RadioGroup
label="Billing period"
value={plan}
onChange={setPlan}
options={[
{ value: 'monthly', label: 'Monthly', hint: '$6 per month' },
{ value: 'yearly', label: 'Yearly', hint: '$48 per year, save 33%' },
]}
/>Three details do the work. The whole row is the Pressable, so tapping the label selects the option and the touch target is 48 points tall instead of a 22-point circle. The radio role plus checkedstate makes VoiceOver and TalkBack announce “Yearly, radio button, selected, 2 of 2”. And the group label tells a screen-reader user what question the options answer.
Variations people usually need next
- Card-style options. For plan pickers, replace the row style with a bordered card and highlight the border when checked. The logic does not change.
- Nothing selected yet. If the user must make an active choice, type the value as
T | null, start atnull, and disable the Continue button until it is set. Do not preselect an option you want people to think about. - Inside a form.Treat the group like any other field: the value goes into your form state and validation reports “Choose a delivery speed” under the group. The form validation guide covers the error display pattern.
- Horizontal layout. Set the container to
flexDirection: 'row'for two short options like Yes / No. Past three options, go back to vertical, because labels wrap badly on small phones.
Radio, checkbox, switch, or dropdown?
Radio buttons are for one choice from a small set the user should compare. Several choices at once is a checkbox list. A single on/off that takes effect immediately is a switch. A long list such as countries or time zones is a dropdown, because ten radio rows push the rest of the form off screen.
If you would rather describe the screen than write it, “a checkout screen with a radio group for standard, express, and next-day delivery, prices on the right”, ShipNative generates the accessible group and the state wiring as real React Native, and runs it on your phone so you can check it with VoiceOver before you ship.