Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6221077
refactor(radio-button): merge platform variants into one MD3 component
burczu Jun 19, 2026
a155b74
feat(radio-button): add MD3 tokens, error state, single animation path
burczu Jun 19, 2026
015baf4
perf(radio-button): memoize group context, switch to useContext
burczu Jun 19, 2026
9649647
fix(radio-button): single a11y node per item, error + parity cleanup
burczu Jun 19, 2026
9e4a668
docs(radio-button): update example + drop platform variant docs
burczu Jun 19, 2026
d9f3973
fix(radio-button): apply disabled opacity, animate group selection
burczu Jun 19, 2026
ee00e84
docs(radio-button): dim disabled labels, add unchecked error examples
burczu Jun 19, 2026
c0e1c18
refactor(radio-button): migrate dot animation to reanimated
burczu Jun 19, 2026
e486e99
fix(radio-button): fix post-rebase CI failures
burczu Jul 2, 2026
2b4eebb
fix(radio-button): fix prettier formatting in utils.ts
burczu Jul 2, 2026
1d1ea02
docs(radio-button): drop deleted pages from _meta.json
burczu Jul 2, 2026
3c226a1
docs(radio-button): remove deleted components from docs config
burczu Jul 2, 2026
ea2a6e9
fix(radio-button): address PR review comments
burczu Jul 3, 2026
a1a3e75
fix: prevent nested item control from receiving focus
MikitasK Aug 24, 2026
feb9a54
fix: use 40dp MD3 state layer
MikitasK Aug 24, 2026
5554631
Merge branch 'main' of https://github.com/callstack/react-native-pape…
MikitasK Aug 24, 2026
12644a8
refactor: address review feedback
MikitasK Aug 25, 2026
e0366bc
docs: add v6 migration guidance
MikitasK Aug 28, 2026
4008f1a
Merge branch 'main' of https://github.com/callstack/react-native-pape…
MikitasK Sep 4, 2026
12e10e1
fix: keep 48dp touch target for Checkbox and RadioButton
MikitasK Sep 4, 2026
1fc4903
test: cover getMinTouchTargetHitSlop
MikitasK Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/6.x/docs/guides/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,22 @@ e.g.:
- The default elevation changed from level `1` to level `3`.
- The `style` prop no longer configures the background color or border radius. You can override `theme.colors.surfaceContainerHigh` and `theme.shapes.corner.extraLarge` using the `theme` prop instead.

### RadioButton

`RadioButton` renders one Material 3 circular radio control on every platform. The platform-specific components and their prop types are gone. On iOS checked controls no longer render a checkmark; they use the same ring-and-dot indicator as other platforms. `RadioButton` now also forwards `TouchableRipple` props (`accessible`, `onFocus`, `onBlur`, `borderless`, `rippleColor`, ...).

| v5 | v6 |
| ------------------------------------------------ | ----------------------------------------------------------------------- |
| `RadioButton.Android`, `RadioButton.IOS` | `RadioButton` |
| `RadioButtonAndroidProps`, `RadioButtonIOSProps` | `RadioButtonProps` |
| `RadioButton.Item` `mode="android" \| "ios"` | `mode` removed; `RadioButton.Item` always renders `RadioButton` |
| n/a | new `error` (uses `theme.colors.error`) and `style` (state-layer style) |

```diff
- <RadioButton.IOS value="first" status="checked" onPress={select} />
+ <RadioButton value="first" status="checked" onPress={select} />
```

### TextInput

The Paper 6.x `TextInput` is a complete rewrite with a new API. Import the component the same way, but note that the props and behavior have changed significantly.
Expand Down
2 changes: 0 additions & 2 deletions docs/component-docs.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,7 @@ const pages = {
ProgressBar: 'ProgressBar',
RadioButton: {
RadioButton: 'RadioButton/RadioButton',
RadioButtonAndroid: 'RadioButton/RadioButtonAndroid',
RadioButtonGroup: 'RadioButton/RadioButtonGroup',
RadioButtonIOS: 'RadioButton/RadioButtonIOS',
RadioButtonItem: 'RadioButton/RadioButtonItem',
},
Searchbar: 'Searchbar',
Expand Down
10 changes: 0 additions & 10 deletions docs/src/data/screenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,23 +107,13 @@ export const screenshots = {
Modal: 'screenshots/modal.gif',
ProgressBar: 'screenshots/progress-bar.png',
RadioButton: {
'Android (enabled)': 'screenshots/radio-enabled.android.png',
'Android (disabled)': 'screenshots/radio-disabled.android.png',
'iOS (enabled)': 'screenshots/radio-enabled.ios.png',
'iOS (disabled)': 'screenshots/radio-disabled.ios.png',
},
'RadioButton.Android': {
enabled: 'screenshots/radio-enabled.android.png',
disabled: 'screenshots/radio-disabled.android.png',
},
'RadioButton.Group': {
Android: 'screenshots/radio-button-group-android.gif',
iOS: 'screenshots/radio-button-group-ios.gif',
},
'RadioButton.IOS': {
enabled: 'screenshots/radio-enabled.ios.png',
disabled: 'screenshots/radio-disabled.ios.png',
},
'RadioButton.Item': 'screenshots/radio-item.ios.png',
Searchbar: {
bar: 'screenshots/searchbar.png',
Expand Down
46 changes: 30 additions & 16 deletions example/src/Examples/RadioButtonExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {

import ScreenWrapper from '../ScreenWrapper';

type State = 'normal' | 'normal-ios' | 'normal-item' | 'custom';
type State = 'normal' | 'normal-item' | 'custom';

const RadioButtonExample = () => {
const [checked, setChecked] = React.useState<State>('normal');
Expand All @@ -19,26 +19,15 @@ const RadioButtonExample = () => {
<ScreenWrapper style={styles.container}>
<TouchableRipple onPress={() => setChecked('normal')}>
<View style={styles.row}>
<Text>Normal - Material Design</Text>
<Text>Normal</Text>
<View pointerEvents="none">
<RadioButton.Android
<RadioButton
value="normal"
status={checked === 'normal' ? 'checked' : 'unchecked'}
/>
</View>
</View>
</TouchableRipple>
<TouchableRipple onPress={() => setChecked('normal-ios')}>
<View style={styles.row}>
<Text>Normal 2 - IOS</Text>
<View pointerEvents="none">
<RadioButton.IOS
value="normal-ios"
status={checked === 'normal-ios' ? 'checked' : 'unchecked'}
/>
</View>
</View>
</TouchableRipple>
<TouchableRipple onPress={() => setChecked('custom')}>
<View style={styles.row}>
<Text>Custom</Text>
Expand All @@ -58,11 +47,31 @@ const RadioButtonExample = () => {
onPress={() => setChecked('normal-item')}
/>
<View style={styles.row}>
<Text>Checked (Disabled)</Text>
<Text>Error (Checked)</Text>
<RadioButton value="error" status="checked" error />
</View>
<View style={styles.row}>
<Text>Error (Unchecked)</Text>
<RadioButton value="error-unchecked" status="unchecked" error />
</View>
<RadioButton.Item
label="Error - Item (Checked)"
value="error-item"
status="checked"
error
/>
<RadioButton.Item
label="Error - Item (Unchecked)"
value="error-item-unchecked"
status="unchecked"
error
/>
<View style={styles.row}>
<Text style={styles.disabledLabel}>Checked (Disabled)</Text>
<RadioButton value="first" status="checked" disabled />
</View>
<View style={styles.row}>
<Text>Unchecked (Disabled)</Text>
<Text style={styles.disabledLabel}>Unchecked (Disabled)</Text>
<RadioButton value="second" status="unchecked" disabled />
</View>
<RadioButton.Item
Expand Down Expand Up @@ -94,6 +103,11 @@ const styles = StyleSheet.create({
paddingVertical: 8,
paddingHorizontal: 16,
},
// Standalone RadioButton has no label of its own; dim the demo label to
// match the disabled control (RadioButton.Item dims its label for you).
disabledLabel: {
opacity: 0.38,
},
});

export default RadioButtonExample;
4 changes: 2 additions & 2 deletions example/src/Examples/RadioButtonGroupExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ const RadioButtonGroupExample = () => {
</View>
<View style={styles.row}>
<Text>Second</Text>
<RadioButton.Android value="second" />
<RadioButton value="second" />
</View>
<View style={styles.row}>
<Text>Third</Text>
<RadioButton.IOS value="third" />
<RadioButton value="third" />
</View>
</RadioButton.Group>
</List.Section>
Expand Down
38 changes: 15 additions & 23 deletions example/src/Examples/RadioButtonItemExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,55 +7,47 @@ import ScreenWrapper from '../ScreenWrapper';

const RadioButtonItemExample = () => {
const [checkedDefault, setCheckedDefault] = React.useState(true);
const [checkedAndroid, setCheckedAndroid] = React.useState(true);
const [checkedIOS, setCheckedIOS] = React.useState(true);
const [checkedLeadingControl, setCheckedLeadingControl] =
React.useState(true);
const [checkedDisabled, setCheckedDisabled] = React.useState<boolean>(true);
const [checkedError, setCheckedError] = React.useState(true);
const [checkedLabelVariant, setCheckedLabelVariant] = React.useState(true);

return (
<ScreenWrapper style={styles.container}>
<RadioButton.Item
label="Default (will look like whatever system this is running on)"
label="Default (trailing control)"
status={checkedDefault ? 'checked' : 'unchecked'}
onPress={() => setCheckedDefault(!checkedDefault)}
value="default"
/>
<RadioButton.Item
label="Material Design"
mode="android"
status={checkedAndroid ? 'checked' : 'unchecked'}
onPress={() => setCheckedAndroid(!checkedAndroid)}
value="android"
/>
<RadioButton.Item
label="iOS"
mode="ios"
status={checkedIOS ? 'checked' : 'unchecked'}
onPress={() => setCheckedIOS(!checkedIOS)}
value="iOS"
/>
<RadioButton.Item
label="Default with leading control"
label="Leading control"
status={checkedLeadingControl ? 'checked' : 'unchecked'}
onPress={() => setCheckedLeadingControl(!checkedLeadingControl)}
value="iOS"
value="leading"
position="leading"
/>
<RadioButton.Item
label="Disabled checkbox"
label="Error"
status={checkedError ? 'checked' : 'unchecked'}
onPress={() => setCheckedError(!checkedError)}
value="error"
error
/>
<RadioButton.Item
label="Disabled"
status={checkedDisabled ? 'checked' : 'unchecked'}
onPress={() => setCheckedDisabled(!checkedDisabled)}
value="iOS"
value="disabled"
disabled
/>
<RadioButton.Item
label="Default with titleLarge title variant"
label="titleLarge label variant"
labelVariant="titleLarge"
status={checkedLabelVariant ? 'checked' : 'unchecked'}
onPress={() => setCheckedLabelVariant(!checkedLabelVariant)}
value="default"
value="variant"
/>
</ScreenWrapper>
);
Expand Down
6 changes: 6 additions & 0 deletions src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext';
import { tokens } from '../../theme/tokens';
import type { ThemeProp } from '../../theme/types';
import { isKeyboardFocusEvent } from '../../utils/isKeyboardFocusEvent';
import { getMinTouchTargetHitSlop } from '../../utils/touchTarget';
import TouchableRipple from '../TouchableRipple/TouchableRipple';
import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple';

Expand Down Expand Up @@ -76,6 +77,10 @@ const {
stateLayerSize: STATE_LAYER_SIZE,
} = CheckboxTokens;

// The state layer is 40dp per spec, which is below the 48dp minimum touch
// target. Expand the pressable area without changing the visual bounds.
const HIT_SLOP = getMinTouchTargetHitSlop(STATE_LAYER_SIZE);

const FOCUS_THICKNESS = tokens.md.sys.state.focusIndicator.thickness;
// Focus indicator is a circular ring at the 40dp state-layer boundary.
// We don't apply `focusIndicator.outerOffset` here because the surrounding
Expand Down Expand Up @@ -234,6 +239,7 @@ const Checkbox = ({

return (
<TouchableRipple
hitSlop={HIT_SLOP}
{...rest}
borderless
centered
Expand Down
Loading