RadioGroup
RadioGroup and RadioGroupItem: a single choice from a list on the SelectionGroup Primitive, vertical or horizontal, with a haptic tick.
Componentforms
Style
Light
Dark
Your app is ready.
Inter body text, Inter headings.
smmdlgxl
you@example.com
Continue
a0: Vega, neutral, neutral accent, default radius, Inter.Change PresetInstallation
$
npx nativecn-cli@latest add radio-groupAlso installs: theme, pressable, selection-group, text.
Usage
RadioGroup demo
A vertical and a horizontal RadioGroup, with a disabled item.
import { useState } from "react";
import { View } from "react-native";
import { RadioGroup, RadioGroupItem } from "@/components/radio-group";
import { createStyles } from "@/theme";
const useStyles = createStyles((t) => ({
root: { gap: t.spacing[6] },
}));
export default function RadioGroupDemo() {
const styles = useStyles();
const [plan, setPlan] = useState("monthly");
return (
<View style={styles.root}>
<RadioGroup value={plan} onValueChange={setPlan}>
<RadioGroupItem value="monthly" label="Monthly" />
<RadioGroupItem value="yearly" label="Yearly" />
<RadioGroupItem value="lifetime" label="Lifetime (sold out)" disabled />
</RadioGroup>
<RadioGroup defaultValue="m" orientation="horizontal">
<RadioGroupItem value="s" label="S" />
<RadioGroupItem value="m" label="M" />
<RadioGroupItem value="l" label="L" />
</RadioGroup>
</View>
);
}RadioGroup demo
A vertical and a horizontal RadioGroup, with a disabled item.
import { useState } from "react";
import { View } from "react-native";
import { RadioGroup, RadioGroupItem } from "@/components/radio-group";
import { createStyles } from "@/theme";
const useStyles = createStyles((t) => ({
root: { gap: t.spacing[6] },
}));
export default function RadioGroupDemo() {
const styles = useStyles();
const [plan, setPlan] = useState("monthly");
return (
<View style={styles.root}>
<RadioGroup value={plan} onValueChange={setPlan}>
<RadioGroupItem value="monthly" label="Monthly" />
<RadioGroupItem value="yearly" label="Yearly" />
<RadioGroupItem value="lifetime" label="Lifetime (sold out)" disabled />
</RadioGroup>
<RadioGroup defaultValue="m" orientation="horizontal">
<RadioGroupItem value="s" label="S" />
<RadioGroupItem value="m" label="M" />
<RadioGroupItem value="l" label="L" />
</RadioGroup>
</View>
);
}Props
RadioGroup
| Prop | Type and description |
|---|---|
value | string: the controlled selected value |
defaultValue | string: the starting value while uncontrolled |
onValueChange | (value: string) => void: called with the newly selected value |
orientation | "vertical" (default) | "horizontal" |
disabled | boolean: disables every item |
style | layout only, merged last onto the root |
RadioGroupItem
| Prop | Type and description |
|---|---|
value | string (required): the value this item selects |
label | string: visible text next to the dot, also the accessible name |
disabled | boolean: this item only |
style | layout only, merged last onto the root |
Accessibility
- Announced as a radiogroup of radio items with aria-checked.
- An item's label is its accessible name.
- Each row is the tap target, at least 48.
- disabled, on the group or an item, is announced as disabled and blocks selection.
Notes
Named exports RadioGroup and RadioGroupItem. A light haptic tick on selection when config.haptics is on. The dot uses the radio.dot Style Slot and the pressed look radio.pressed.
Source
The Vega Style's code, exactly as add copies it into your app.
{components}/radio-group.tsx
import { View, type GestureResponderEvent, type StyleProp, type ViewStyle } from "react-native";
import { Pressable, type PressableProps } from "@/components/primitives/pressable";
import {
SelectionGroup,
useSelectionItem,
type SelectionGroupSingleProps,
} from "@/components/primitives/selection-group";
import { Text } from "@/components/text";
import { createStyles } from "@/theme";
export type RadioGroupOrientation = "vertical" | "horizontal";
export type RadioGroupProps = Omit<SelectionGroupSingleProps, "type" | "itemRole" | "style"> & {
/** Controlled selected value. */
value?: string;
/** Starting value while uncontrolled. */
defaultValue?: string;
/** Called with the newly selected value. */
onValueChange?: (value: string) => void;
/** How the items are laid out (default `vertical`). */
orientation?: RadioGroupOrientation;
/** Layout only, merged last onto the root. */
style?: StyleProp<ViewStyle>;
};
export type RadioGroupItemProps = Omit<
PressableProps,
"children" | "style" | "size" | "pressedStyle" | "haptic" | "loading" | "role"
> & {
/** The value this item selects in its RadioGroup. */
value: string;
/** Visible text next to the dot; it is also the accessible name. */
label?: string;
disabled?: boolean;
/** Layout only, merged last onto the root. */
style?: StyleProp<ViewStyle>;
};
const useStyles = createStyles((t) => {
const dot = { width: t.scaleValue(20), height: t.scaleValue(20) };
return {
group: { gap: t.spacing[3] },
vertical: { flexDirection: "column" },
horizontal: { flexDirection: "row", flexWrap: "wrap", gap: t.spacing[4] },
item: {
flexDirection: "row",
alignItems: "center",
gap: t.spacing[2],
alignSelf: "flex-start",
},
dot: {
...dot,
alignItems: "center",
justifyContent: "center",
borderRadius: t.radius.full,
borderWidth: t.borderWidth.default,
borderColor: t.colors.input,
backgroundColor: t.colors.background,
},
dotOn: { borderColor: t.colors.primary },
// The inner dot is half the outer one.
inner: {
width: dot.width / 2,
height: dot.height / 2,
borderRadius: t.radius.full,
backgroundColor: t.colors.primary,
},
pressed: { opacity: 0.7 },
disabled: { opacity: t.opacity.disabled },
};
});
/**
* A single choice from a list, built on the SelectionGroup Primitive: announced as a
* `radiogroup` of `radio` items with `aria-checked`. Works controlled (`value` +
* `onValueChange`) or uncontrolled (`defaultValue`); `disabled` disables every item.
*/
export function RadioGroup({ orientation = "vertical", style, ...props }: RadioGroupProps) {
const styles = useStyles();
return (
<SelectionGroup
type="single"
itemRole="radio"
style={[styles.group, styles[orientation], style]}
{...props}
/>
);
}
/**
* One option of a RadioGroup: a dot and an optional label. The whole row is the tap target
* (at least 48) and gives a light haptic tick when `config.haptics` is on.
*/
export function RadioGroupItem({
value,
label,
disabled: ownDisabled = false,
onPress,
style,
...props
}: RadioGroupItemProps) {
const styles = useStyles();
const { selected, disabled, itemProps } = useSelectionItem({ value, disabled: ownDisabled });
const handlePress = (event: GestureResponderEvent) => {
itemProps.onPress();
onPress?.(event);
};
return (
<Pressable
{...itemProps}
haptic="selection"
size={label == null ? { width: styles.dot.width, height: styles.dot.height } : undefined}
pressedStyle={styles.pressed}
style={[styles.item, disabled ? styles.disabled : null, style]}
{...props}
onPress={handlePress}
>
<View style={[styles.dot, selected ? styles.dotOn : null]}>
{selected ? <View style={styles.inner} /> : null}
</View>
{label != null ? <Text variant="label">{label}</Text> : null}
</Pressable>
);
}{components}/radio-group.tsx
import { View, type GestureResponderEvent, type StyleProp, type ViewStyle } from "react-native";
import { Pressable, type PressableProps } from "@/components/primitives/pressable";
import {
SelectionGroup,
useSelectionItem,
type SelectionGroupSingleProps,
} from "@/components/primitives/selection-group";
import { Text } from "@/components/text";
import { createStyles } from "@/theme";
export type RadioGroupOrientation = "vertical" | "horizontal";
export type RadioGroupProps = Omit<SelectionGroupSingleProps, "type" | "itemRole" | "style"> & {
/** Controlled selected value. */
value?: string;
/** Starting value while uncontrolled. */
defaultValue?: string;
/** Called with the newly selected value. */
onValueChange?: (value: string) => void;
/** How the items are laid out (default `vertical`). */
orientation?: RadioGroupOrientation;
/** Layout only, merged last onto the root. */
style?: StyleProp<ViewStyle>;
};
export type RadioGroupItemProps = Omit<
PressableProps,
"children" | "style" | "size" | "pressedStyle" | "haptic" | "loading" | "role"
> & {
/** The value this item selects in its RadioGroup. */
value: string;
/** Visible text next to the dot; it is also the accessible name. */
label?: string;
disabled?: boolean;
/** Layout only, merged last onto the root. */
style?: StyleProp<ViewStyle>;
};
const useStyles = createStyles((t) => {
const dot = { width: t.scaleValue(18), height: t.scaleValue(18) };
return {
group: { gap: t.spacing[3] },
vertical: { flexDirection: "column" },
horizontal: { flexDirection: "row", flexWrap: "wrap", gap: t.spacing[4] },
item: {
flexDirection: "row",
alignItems: "center",
gap: t.spacing[2],
alignSelf: "flex-start",
},
dot: {
...dot,
alignItems: "center",
justifyContent: "center",
borderRadius: t.radius.full,
borderWidth: t.borderWidth.default,
borderColor: t.colors.input,
backgroundColor: t.colors.background,
},
dotOn: { borderColor: t.colors.primary },
// The inner dot is half the outer one.
inner: {
width: dot.width / 2,
height: dot.height / 2,
borderRadius: t.radius.full,
backgroundColor: t.colors.primary,
},
pressed: { opacity: 0.7 },
disabled: { opacity: t.opacity.disabled },
};
});
/**
* A single choice from a list, built on the SelectionGroup Primitive: announced as a
* `radiogroup` of `radio` items with `aria-checked`. Works controlled (`value` +
* `onValueChange`) or uncontrolled (`defaultValue`); `disabled` disables every item.
*/
export function RadioGroup({ orientation = "vertical", style, ...props }: RadioGroupProps) {
const styles = useStyles();
return (
<SelectionGroup
type="single"
itemRole="radio"
style={[styles.group, styles[orientation], style]}
{...props}
/>
);
}
/**
* One option of a RadioGroup: a dot and an optional label. The whole row is the tap target
* (at least 48) and gives a light haptic tick when `config.haptics` is on.
*/
export function RadioGroupItem({
value,
label,
disabled: ownDisabled = false,
onPress,
style,
...props
}: RadioGroupItemProps) {
const styles = useStyles();
const { selected, disabled, itemProps } = useSelectionItem({ value, disabled: ownDisabled });
const handlePress = (event: GestureResponderEvent) => {
itemProps.onPress();
onPress?.(event);
};
return (
<Pressable
{...itemProps}
haptic="selection"
size={label == null ? { width: styles.dot.width, height: styles.dot.height } : undefined}
pressedStyle={styles.pressed}
style={[styles.item, disabled ? styles.disabled : null, style]}
{...props}
onPress={handlePress}
>
<View style={[styles.dot, selected ? styles.dotOn : null]}>
{selected ? <View style={styles.inner} /> : null}
</View>
{label != null ? <Text variant="label">{label}</Text> : null}
</Pressable>
);
}
