Created
March 18, 2025 06:47
-
-
Save kevnoutsawo/d5779c71b703d58f13833e2e268dbea8 to your computer and use it in GitHub Desktop.
React Native animated switch component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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