Radio Group Primitive
A group of selectable buttons, commonly referred to as radio buttons, wherein only one button can be checked simultaneously.
Installation
Install the component via your command line.
npx expo install @rn-primitives/radio-group
Install @radix-ui/react-radio-group
npx expo install @radix-ui/react-radio-group
Copy/paste the following code for web to ~/components/primitives/radio-group/radio-group.web.tsx
import * as RadioGroup from '@radix-ui/react-radio-group';import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { GestureResponderEvent, Pressable, View } from 'react-native';import type { IndicatorProps, IndicatorRef, ItemProps, ItemRef, RootProps, RootRef } from './types';
const RadioGroupContext = React.createContext<RootProps | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ({ asChild, value, onValueChange, disabled = false, ...viewProps }, ref) => { const Component = asChild ? Slot.View : View; return ( <RadioGroupContext.Provider value={{ value, disabled, onValueChange, }} > <RadioGroup.Root value={value} onValueChange={onValueChange} disabled={disabled} asChild> <Component ref={ref} {...viewProps} /> </RadioGroup.Root> </RadioGroupContext.Provider> ); });
Root.displayName = 'RootRadioGroup';function useRadioGroupContext() { const context = React.useContext(RadioGroupContext); if (!context) { throw new Error( 'RadioGroup compound components cannot be rendered outside the RadioGroup component' ); } return context;}const Item = React.forwardRef<ItemRef, ItemProps>( ({ asChild, value, onPress: onPressProps, ...props }, ref) => { const { onValueChange } = useRadioGroupContext();
function onPress(ev: GestureResponderEvent) { if (onPressProps) { onPressProps(ev); } onValueChange(value); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <RadioGroup.Item value={value} asChild> <Component ref={ref} onPress={onPress} {...props} /> </RadioGroup.Item> ); });
Item.displayName = 'ItemRadioGroup';
const Indicator = React.forwardRef<IndicatorRef, IndicatorProps>( ({ asChild, forceMount, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <RadioGroup.Indicator asChild> <Component ref={ref} {...props} /> </RadioGroup.Indicator> ); });
Indicator.displayName = 'IndicatorRadioGroup';
export { Indicator, Item, Root };
Copy/paste the following code for native to ~/components/primitives/radio-group/radio-group.tsx
import * as React from 'react';import { Pressable, View, type GestureResponderEvent } from 'react-native';import * as Slot from '~/components/primitives/slot';import type { IndicatorProps, IndicatorRef, ItemProps, ItemRef, RootProps, RootRef } from './types';
const RadioGroupContext = React.createContext<RootProps | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ({ asChild, value, onValueChange, disabled = false, ...viewProps }, ref) => { const Component = asChild ? Slot.View : View; return ( <RadioGroupContext.Provider value={{ value, disabled, onValueChange, }} > <Component ref={ref} role='radiogroup' {...viewProps} /> </RadioGroupContext.Provider> ); });
Root.displayName = 'RootRadioGroup';
function useRadioGroupContext() { const context = React.useContext(RadioGroupContext); if (!context) { throw new Error( 'RadioGroup compound components cannot be rendered outside the RadioGroup component' ); } return context;}
interface RadioItemContext { itemValue: string | undefined;}
const RadioItemContext = React.createContext<RadioItemContext | null>(null);
const Item = React.forwardRef<ItemRef, ItemProps>( ( { asChild, value: itemValue, disabled: disabledProp = false, onPress: onPressProp, ...props }, ref ) => { const { disabled, value, onValueChange } = useRadioGroupContext();
function onPress(ev: GestureResponderEvent) { if (disabled || disabledProp) return; onValueChange(itemValue); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <RadioItemContext.Provider value={{ itemValue: itemValue, }} > <Component ref={ref} role='radio' onPress={onPress} aria-checked={value === itemValue} disabled={(disabled || disabledProp) ?? false} accessibilityState={{ disabled: (disabled || disabledProp) ?? false, checked: value === itemValue, }} {...props} /> </RadioItemContext.Provider> ); });
Item.displayName = 'ItemRadioGroup';
function useRadioItemContext() { const context = React.useContext(RadioItemContext); if (!context) { throw new Error( 'RadioItem compound components cannot be rendered outside the RadioItem component' ); } return context;}
const Indicator = React.forwardRef<IndicatorRef, IndicatorProps>( ({ asChild, forceMount, ...props }, ref) => { const { value } = useRadioGroupContext(); const { itemValue } = useRadioItemContext();
if (!forceMount) { if (value !== itemValue) { return null; } } const Component = asChild ? Slot.View : View; return <Component ref={ref} role='presentation' {...props} />; });
Indicator.displayName = 'IndicatorRadioGroup';
export { Indicator, Item, Root };
Copy/paste the following code for types to ~/components/primitives/radio-group/types.ts
import { ForceMountable, PressableRef, SlottablePressableProps, SlottableViewProps, ViewRef,} from '~/components/primitives/types';
type RootProps = SlottableViewProps & { value: string | undefined; onValueChange: (val: string) => void; disabled?: boolean;};
type ItemProps = SlottablePressableProps & { value: string; /** * nativeID of the label element that describes this radio group item */ 'aria-labelledby'?: string;};
type IndicatorProps = SlottableViewProps & ForceMountable;
type RootRef = ViewRef;type ItemRef = PressableRef;type IndicatorRef = ViewRef;
export type { IndicatorProps, IndicatorRef, ItemProps, ItemRef, RootProps, RootRef };
Copy/paste the following code for exporting to ~/components/primitives/radio-group/index.ts
export * from './radio-group';export * from './types';
Copy/paste the following code for native to ~/components/primitives/radio-group/index.tsx
import * as React from 'react';import { Pressable, View, type GestureResponderEvent } from 'react-native';import * as Slot from '~/components/primitives/slot';import type { IndicatorProps, IndicatorRef, ItemProps, ItemRef, RootProps, RootRef } from './types';
const RadioGroupContext = React.createContext<RootProps | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ({ asChild, value, onValueChange, disabled = false, ...viewProps }, ref) => { const Component = asChild ? Slot.View : View; return ( <RadioGroupContext.Provider value={{ value, disabled, onValueChange, }} > <Component ref={ref} role='radiogroup' {...viewProps} /> </RadioGroupContext.Provider> ); });
Root.displayName = 'RootRadioGroup';
function useRadioGroupContext() { const context = React.useContext(RadioGroupContext); if (!context) { throw new Error( 'RadioGroup compound components cannot be rendered outside the RadioGroup component' ); } return context;}
interface RadioItemContext { itemValue: string | undefined;}
const RadioItemContext = React.createContext<RadioItemContext | null>(null);
const Item = React.forwardRef<ItemRef, ItemProps>( ( { asChild, value: itemValue, disabled: disabledProp = false, onPress: onPressProp, ...props }, ref ) => { const { disabled, value, onValueChange } = useRadioGroupContext();
function onPress(ev: GestureResponderEvent) { if (disabled || disabledProp) return; onValueChange(itemValue); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <RadioItemContext.Provider value={{ itemValue: itemValue, }} > <Component ref={ref} role='radio' onPress={onPress} aria-checked={value === itemValue} disabled={(disabled || disabledProp) ?? false} accessibilityState={{ disabled: (disabled || disabledProp) ?? false, checked: value === itemValue, }} {...props} /> </RadioItemContext.Provider> ); });
Item.displayName = 'ItemRadioGroup';
function useRadioItemContext() { const context = React.useContext(RadioItemContext); if (!context) { throw new Error( 'RadioItem compound components cannot be rendered outside the RadioItem component' ); } return context;}
const Indicator = React.forwardRef<IndicatorRef, IndicatorProps>( ({ asChild, forceMount, ...props }, ref) => { const { value } = useRadioGroupContext(); const { itemValue } = useRadioItemContext();
if (!forceMount) { if (value !== itemValue) { return null; } } const Component = asChild ? Slot.View : View; return <Component ref={ref} role='presentation' {...props} />; });
Indicator.displayName = 'IndicatorRadioGroup';
export { Indicator, Item, Root };
Copy/paste the following code for types to ~/components/primitives/radio-group/types.ts
import { ForceMountable, PressableRef, SlottablePressableProps, SlottableViewProps, ViewRef,} from '~/components/primitives/types';
type RootProps = SlottableViewProps & { value: string | undefined; onValueChange: (val: string) => void; disabled?: boolean;};
type ItemProps = SlottablePressableProps & { value: string; /** * nativeID of the label element that describes this radio group item */ 'aria-labelledby'?: string;};
type IndicatorProps = SlottableViewProps & ForceMountable;
type RootRef = ViewRef;type ItemRef = PressableRef;type IndicatorRef = ViewRef;
export type { IndicatorProps, IndicatorRef, ItemProps, ItemRef, RootProps, RootRef };
Usage
import * as React from 'react';import * as RadioGroupPrimitive from '@rn-primitives/radio-group';import { Text, View } from 'react-native';
function Example() { const [value, setValue] = React.useState('Comfortable');
function onLabelPress(label: string) { return () => { setValue(label); }; } return ( <RadioGroupPrimitive.Root value={value} onValueChange={setValue}> <View> <RadioGroupPrimitive.Item value='Default' aria-labelledby='default-label'> <RadioGroupPrimitive.Indicator /> </RadioGroupPrimitive.Item> <Text nativeID='default-label' onPress={onLabelPress('Default')}>Default</Text> </View> <View> <RadioGroupPrimitive.Item value='Comfortable' aria-labelledby='comfortable-label'> <RadioGroupPrimitive.Indicator /> </RadioGroupPrimitive.Item> <Text nativeID='comfortable-label' onPress={onLabelPress('Comfortable')}>Comfortable</Text> </View> <View> <RadioGroupPrimitive.Item value='Compact' aria-labelledby='compact-label'> <RadioGroupPrimitive.Indicator /> </RadioGroupPrimitive.Item> <Text nativeID='compact-label' onPress={onLabelPress('Compact')}>Compact</Text> </View> </RadioGroupPrimitive.Root> );}
Props
Root
Extends View
props
Prop | Type | Note |
---|---|---|
value | string | undefined | |
onValueChange | (val: string) => void | |
asChild | boolean | (optional) |
disabled | boolean | (optional) |
Item
Extends Pressable
props
Prop | Type | Note |
---|---|---|
value | string | |
aria-labelledby | string | Its label’s nativeID |
asChild | boolean | (optional) |
Indicator
Extends View
props
Prop | Type | Note |
---|---|---|
forceMount | boolean | (optional) |
asChild | boolean | (optional) |