Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 51 additions & 0 deletions docs/6.x/docs/guides/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,54 @@ mutually exclusive options, use `SegmentedButtons`.
/>
</>
```

### Switch

#### Explicit operability

A `Switch` now has to declare how it can be operated. Previously a switch with no `onValueChange` still rendered as an enabled, focusable control that did nothing when activated, and screen readers announced it as operable.

Pass `onValueChange` to make it interactive, or mark it `readOnly` or `disabled` to render it as a state indicator. A read-only switch keeps its enabled appearance and exposes its on/off state to screen readers, but it cannot be focused with a keyboard or pressed. On web it exposes `aria-readonly`. On iOS and Android it exposes a disabled accessibility state because React Native has no read-only state for switches. Use `disabled` when you also want the disabled appearance.

```tsx
// Before (v5) — an enabled switch that does nothing
<Switch value={isDarkTheme} />

// After (v6) — say which it is
<Switch value={isDarkTheme} onValueChange={setIsDarkTheme} />
<Switch value={isDarkTheme} readOnly />
<Switch value={isDarkTheme} disabled />
```

This most often shows up where the switch sits inside a row that owns the press:

```tsx
// After (v6)
<TouchableRipple onPress={toggleTheme}>
<View style={styles.row}>
<Text>Dark Theme</Text>
<View pointerEvents="none">
<Switch value={isDarkTheme} readOnly />
</View>
</View>
</TouchableRipple>
Comment on lines +383 to +390

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this pattern the row is the control, but screen readers do not get the on/off state from it. The drawer rows in the example app use the same structure:

  • iOS: the row is one element with no switch trait and no value. The inner switch is merged into it, so its on/off state is lost.
  • Android: the row is a clickable element with no checked state, and the inner switch is a separate disabled switch next to it.
  • Web: the focusable row has no role and no aria-checked.

Give the row the switch role and state, and hide the inner switch:

Suggested change
<TouchableRipple onPress={toggleTheme}>
<View style={styles.row}>
<Text>Dark Theme</Text>
<View pointerEvents="none">
<Switch value={isDarkTheme} readOnly />
</View>
</View>
</TouchableRipple>
<TouchableRipple
onPress={toggleTheme}
role="switch"
aria-checked={isDarkTheme}
>
<View style={styles.row}>
<Text>Dark Theme</Text>
<View pointerEvents="none" aria-hidden>
<Switch value={isDarkTheme} readOnly />
</View>
</View>
</TouchableRipple>

I tried this on the "Dark Theme" drawer row. iOS then shows one switch "Dark Theme" with a value that follows the toggle. Android shows one checkable switch "Dark Theme" and the inner switch drops out of the accessibility tree. Web shows role="switch" and aria-checked on the row. The drawer, FAB and TextInput example rows can use the same change. On web, Space still does not toggle the row, because React Native Web handles Space only for button roles. Enter works. (via Claude, verify if it makes sense)

```

#### Selected icon color

The icon inside a selected switch now uses `onPrimaryContainer` instead of `primary`, matching the Material Design 3 spec and improving its contrast against the handle. If you theme `Switch` with a partial `theme.colors` override, add `onPrimaryContainer` alongside the roles you already override — omitted roles fall back to the app theme, which would leave the icon in the default palette.

```diff
const switchTheme = {
colors: {
primary: theme.colors.tertiary,
onPrimary: theme.colors.onTertiary,
primaryContainer: theme.colors.tertiaryContainer,
+ onPrimaryContainer: theme.colors.onTertiaryContainer,
},
};
```

#### Touch target height

`Switch` now reserves the 48dp minimum touch target Material Design requires, so it occupies 48dp of height instead of 40dp. Nothing painted changed size — the track is still 52×32 — but rows containing a switch may become slightly taller.
12 changes: 6 additions & 6 deletions example/src/DrawerItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ function DrawerItems() {
<View style={[styles.preference, styles.v3Preference]}>
<Text variant="labelLarge">Use Dynamic Theme</Text>
<View pointerEvents="none">
<Switch value={shouldUseDynamicTheme} />
<Switch value={shouldUseDynamicTheme} readOnly />
</View>
</View>
</TouchableRipple>
Expand All @@ -186,7 +186,7 @@ function DrawerItems() {
<View style={[styles.preference, styles.v3Preference]}>
<Text variant="labelLarge">Dark Theme</Text>
<View pointerEvents="none">
<Switch value={isDarkTheme} />
<Switch value={isDarkTheme} readOnly />
</View>
</View>
</TouchableRipple>
Expand All @@ -195,7 +195,7 @@ function DrawerItems() {
<View style={[styles.preference, styles.v3Preference]}>
<Text variant="labelLarge">RTL</Text>
<View pointerEvents="none">
<Switch value={isRTL} />
<Switch value={isRTL} readOnly />
</View>
</View>
</TouchableRipple>
Expand All @@ -204,7 +204,7 @@ function DrawerItems() {
<View style={[styles.preference, styles.v3Preference]}>
<Text variant="labelLarge">Collapsed drawer *</Text>
<View pointerEvents="none">
<Switch value={collapsed} />
<Switch value={collapsed} readOnly />
</View>
</View>
</TouchableRipple>
Expand All @@ -213,7 +213,7 @@ function DrawerItems() {
<View style={[styles.preference, styles.v3Preference]}>
<Text variant="labelLarge">Custom font *</Text>
<View pointerEvents="none">
<Switch value={customFontLoaded} />
<Switch value={customFontLoaded} readOnly />
</View>
</View>
</TouchableRipple>
Expand All @@ -224,7 +224,7 @@ function DrawerItems() {
{Platform.OS === 'ios' ? 'Highlight' : 'Ripple'} effect *
</Text>
<View pointerEvents="none">
<Switch value={rippleEffectEnabled} />
<Switch value={rippleEffectEnabled} readOnly />
</View>
</View>
</TouchableRipple>
Expand Down
2 changes: 1 addition & 1 deletion example/src/Examples/FABExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ const FABExample = () => {
title="Show FAB"
right={() => (
<View pointerEvents="none">
<Switch value={showFab} />
<Switch value={showFab} readOnly />
</View>
)}
onPress={() => setShowFab((v) => !v)}
Expand Down
5 changes: 5 additions & 0 deletions example/src/Examples/SwitchExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const SwitchExample = () => {
primary: theme.colors.tertiary,
onPrimary: theme.colors.onTertiary,
primaryContainer: theme.colors.tertiaryContainer,
onPrimaryContainer: theme.colors.onTertiaryContainer,
secondary: theme.colors.tertiary,
},
}),
Expand Down Expand Up @@ -88,6 +89,10 @@ const SwitchExample = () => {
/>
</Row>

<Row label="Read-only">
<Switch value={defaultOn} readOnly />
</Row>

<View
style={[
styles.separator,
Expand Down
2 changes: 1 addition & 1 deletion example/src/Examples/TextInputExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ const TextInputDemo = ({ variant }: TextInputDemoProps) => {
<View style={styles.switchRow}>
<Text variant="bodyMedium">{label}</Text>
<View pointerEvents="none">
<Switch value={controls[key]} />
<Switch value={controls[key]} readOnly />
</View>
</View>
</TouchableRipple>
Expand Down
Loading
Loading