Skip to content

Instantly share code, notes, and snippets.

@kevnoutsawo
Created March 18, 2025 06:47
Show Gist options
  • Save kevnoutsawo/d5779c71b703d58f13833e2e268dbea8 to your computer and use it in GitHub Desktop.
Save kevnoutsawo/d5779c71b703d58f13833e2e268dbea8 to your computer and use it in GitHub Desktop.
React Native animated switch component
// Inspiration: https://dribbble.com/shots/4167815-Switcher-XXXIV
// Feel free to reuse in your projects
import React, { memo, useMemo, useCallback } from 'react';
import { Pressable, StyleSheet, View } from 'react-native';
import { MotiView } from 'moti';
import Animated, {
Easing,
useSharedValue,
useAnimatedStyle,
withTiming,
runOnJS,
interpolateColor
} from 'react-native-reanimated';
// Colors extracted to constants
const COLORS = {
inactive: '#000000',
active: '#DCDCDC',
};
// Animation config
const ANIMATION_CONFIG = {
duration: 300,
easing: Easing.inOut(Easing.ease),
};
type SwitchProps = {
size: number;
onPress?: (isActive: boolean) => void;
initialValue?: boolean;
};
const Switch: React.FC<SwitchProps> = memo(({
size,
onPress,
initialValue = false
}) => {
// Shared value to control the animation state (0 = inactive, 1 = active)
const progress = useSharedValue(initialValue ? 1 : 0);
// Calculate dimensions once
const dimensions = useMemo(() => ({
trackWidth: size * 1.5,
trackHeight: size * 0.4,
knobSize: size * 0.6,
borderWidth: size * 0.1,
}), [size]);
// Toggle animation without interrupting current animation
const toggleSwitch = useCallback(() => {
// Determine target value (if currently animating, we'll go to the opposite of the current target)
const toValue = progress.value > 0.5 ? 0 : 1;
// Animate to the new value
progress.value = withTiming(toValue, ANIMATION_CONFIG, (finished) => {
// Call the onPress callback with the new state when animation completes
if (finished && onPress) {
runOnJS(onPress)(toValue === 1);
}
});
}, [progress, onPress]);
// Animated styles
const trackAnimatedStyle = useAnimatedStyle(() => {
return {
backgroundColor: interpolateColor(
progress.value,
[0, 1],
[COLORS.active, COLORS.inactive]
),
};
});
const knobAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [{
translateX: withTiming(
progress.value * (dimensions.trackWidth / 2) - dimensions.trackWidth / 4,
ANIMATION_CONFIG
)
}],
};
});
const innerKnobAnimatedStyle = useAnimatedStyle(() => {
return {
width: withTiming(
progress.value === 0 ? dimensions.knobSize : 0,
ANIMATION_CONFIG
),
borderColor: interpolateColor(
progress.value,
[0, 1],
[COLORS.active, COLORS.inactive]
),
};
});
// Pre-calculate static styles
const containerStyle = useMemo(() => ({
alignItems: 'center',
justifyContent: 'center',
height: size,
}), [size]);
const trackStyle = useMemo(() => ({
position: 'absolute',
width: dimensions.trackWidth,
height: dimensions.trackHeight,
borderRadius: dimensions.trackHeight / 2,
}), [dimensions]);
const knobContainerStyle = useMemo(() => ({
width: size,
height: size,
borderRadius: size / 2,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
}), [size]);
const knobStyle = useMemo(() => ({
height: dimensions.knobSize,
borderRadius: dimensions.knobSize / 2,
borderWidth: dimensions.borderWidth,
borderColor: COLORS.inactive,
}), [dimensions]);
return (
<Pressable onPress={toggleSwitch}>
<View style={containerStyle}>
<Animated.View
style={[trackStyle, trackAnimatedStyle]}
/>
<Animated.View
style={[knobContainerStyle, knobAnimatedStyle]}
>
<Animated.View
style={[knobStyle, innerKnobAnimatedStyle]}
/>
</Animated.View>
</View>
</Pressable>
);
});
// App component using the improved Switch
const App = () => {
// We could still use useState if we need to track the state elsewhere in the app
const [isActive, setIsActive] = React.useState(false);
const handleToggle = useCallback((newState: boolean) => {
setIsActive(newState);
console.log('Switch toggled:', newState);
}, []);
return (
<View style={styles.container}>
<Switch
size={120}
onPress={handleToggle}
initialValue={isActive}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f3f3f4',
alignItems: 'center',
justifyContent: 'center',
},
});
export default memo(App);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment