Browse React Native
React NativeComponentsNo. 17 of 22
Switch
An on/off control whose thumb springs across while the track fades to the primary colour, in two sizes with a 44 px touch area.
For the web: Switch, with the same name and variants.
Preview
Loading the preview
The gallery's web build, through react-native-web. On a phone the same code draws native views.
Installation
npx shadcn@latest add @corsair-native/switchThe app's components.json needs the @corsair-native registry first; see Installation. The files land in your project as source: edit them like your own code.
- Also adds
- themeuse-reduced-motion
API
Read from the source's own comments and types. Props marked * are required.
SwitchComponent
An on/off control for settings that take effect at once. The thumb springs across on the native thread while the track fades to the primary colour; with Reduce Motion it jumps. Screen readers hear a switch, on or off; name it with accessibilityLabel. Controlled with checked / onCheckedChange or uncontrolled with defaultChecked.
| Prop | Type | Default |
|---|---|---|
checked | boolean | — |
defaultChecked | boolean | false |
onCheckedChange | (checked: boolean) => void | — |
size | "sm" | "default" | "default" |
style | StyleProp<ViewStyle> | — |
ref | Ref<View> | — |
Also takes every PressableProps prop (React Native docs), except children, onPress.
<Switch accessibilityLabel="Game reminders" checked={reminders} onCheckedChange={setReminders} />Compatibility
- Expo SDK
- 54 and 57, both checked in CI
- React Native
- 0.81 and 0.86, New Architecture
- Web
- Expo's web target, through react-native-web
- Motion
- Follows Reduce Motion
Source
1 file, installed as source you own
switch.tsxView on GitHubimport { useEffect, useState, type Ref } from "react";
import {
Animated,
I18nManager,
Pressable,
StyleSheet,
type PressableProps,
type StyleProp,
type View,
type ViewStyle,
} from "react-native";
import { useReducedMotion } from "@/registry/native/hooks/use-reduced-motion";
import { useTheme } from "@/registry/native/lib/theme";
type SwitchSize = "sm" | "default";
const SIZE: Record<SwitchSize, { width: number; height: number; thumb: number }> = {
sm: { width: 36, height: 20, thumb: 16 },
default: { width: 48, height: 28, thumb: 24 },
};
const INSET = 2;
const TOUCH = 44;
interface SwitchProps extends Omit<PressableProps, "children" | "style" | "onPress"> {
checked?: boolean;
defaultChecked?: boolean;
onCheckedChange?: (checked: boolean) => void;
size?: SwitchSize;
style?: StyleProp<ViewStyle>;
ref?: Ref<View>;
}
/**
* An on/off control for settings that take effect at once. The thumb springs
* across on the native thread while the track fades to the primary colour;
* with Reduce Motion it jumps. Screen readers hear a switch, on or off; name
* it with `accessibilityLabel`. Controlled with `checked` / `onCheckedChange`
* or uncontrolled with `defaultChecked`.
*
* @example
* <Switch accessibilityLabel="Game reminders" checked={reminders} onCheckedChange={setReminders} />
*/
function Switch({
checked: checkedProp,
defaultChecked = false,
onCheckedChange,
size = "default",
disabled,
style,
hitSlop,
...props
}: SwitchProps) {
const { colors, scheme, motion } = useTheme();
const reduced = useReducedMotion();
const [uncontrolled, setUncontrolled] = useState(defaultChecked);
const checked = checkedProp ?? uncontrolled;
// Not useAnimatedValue, which react-native-web does not have.
const [progress] = useState(() => new Animated.Value(checked ? 1 : 0));
const { width, height, thumb } = SIZE[size];
const travel = (width - thumb - INSET * 2) * (I18nManager.isRTL ? -1 : 1);
useEffect(() => {
if (reduced) {
progress.setValue(checked ? 1 : 0);
return;
}
const spring = Animated.spring(progress, {
toValue: checked ? 1 : 0,
...motion.spring.snappy,
useNativeDriver: true,
});
spring.start();
return () => spring.stop();
}, [checked, motion, progress, reduced]);
// In dark mode the thumb follows the web theme: foreground off, primary-foreground on.
const thumbOff = scheme === "dark" ? colors.foreground : colors.background;
const thumbOn = scheme === "dark" ? colors.primaryForeground : colors.background;
return (
<Pressable
role="switch"
aria-checked={checked}
disabled={disabled}
hitSlop={hitSlop ?? Math.max(0, (TOUCH - height) / 2)}
onPress={() => {
const next = !checked;
if (checkedProp === undefined) setUncontrolled(next);
onCheckedChange?.(next);
}}
style={[
styles.track,
{ width, height, backgroundColor: colors.input },
disabled ? styles.disabled : null,
style,
]}
{...props}
>
<Animated.View
style={[
StyleSheet.absoluteFill,
styles.round,
{ backgroundColor: colors.primary, opacity: progress },
]}
/>
<Animated.View
style={[
styles.round,
styles.thumb,
{
width: thumb,
height: thumb,
backgroundColor: thumbOff,
transform: [
{
translateX: progress.interpolate({ inputRange: [0, 1], outputRange: [0, travel] }),
},
],
},
]}
>
{thumbOn === thumbOff ? null : (
<Animated.View
style={[
StyleSheet.absoluteFill,
styles.round,
{ backgroundColor: thumbOn, opacity: progress },
]}
/>
)}
</Animated.View>
</Pressable>
);
}
const styles = StyleSheet.create({
track: {
justifyContent: "center",
borderRadius: 9999,
paddingHorizontal: INSET,
overflow: "hidden",
},
round: {
borderRadius: 9999,
},
thumb: {
boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.15)",
},
disabled: {
opacity: 0.5,
},
});
export { Switch, type SwitchProps, type SwitchSize };