Skip to content

Instantly share code, notes, and snippets.

@ashaffah
Created October 9, 2025 05:33
Show Gist options
  • Select an option

  • Save ashaffah/14f7f9381e6d1d8678be456ec25463fb to your computer and use it in GitHub Desktop.

Select an option

Save ashaffah/14f7f9381e6d1d8678be456ec25463fb to your computer and use it in GitHub Desktop.
React Native Expo Camera Modal Component
import {
StyleSheet,
Text,
TouchableOpacity,
View,
Modal,
Animated,
Dimensions,
} from 'react-native';
import React, {useRef, useState, useEffect} from 'react';
import {CameraView, type FlashMode} from 'expo-camera';
import IonicIcons from 'react-native-vector-icons/Ionicons';
export default function CameraModal({visible, onClose, onBarcodeScanned}: any) {
const cameraRef = useRef<null>(null);
const [flash, setFlash] = useState<FlashMode>('off');
const scanLineAnim = useRef(new Animated.Value(0)).current;
const screenWidth = Dimensions.get('window').width;
const screenHeight = Dimensions.get('window').height;
const scanPositionTop = (screenHeight - SCAN_AREA_SIZE) / 2;
const scanPositionLeft = (screenWidth - SCAN_AREA_SIZE) / 2;
useEffect(() => {
if (visible) {
Animated.loop(
Animated.sequence([
Animated.timing(scanLineAnim, {
toValue: 1,
duration: 1500,
useNativeDriver: true,
}),
Animated.timing(scanLineAnim, {
toValue: 0,
duration: 1500,
useNativeDriver: true,
}),
]),
).start();
} else {
scanLineAnim.setValue(0);
}
}, [visible, scanLineAnim]);
const scanLineTranslate = scanLineAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, SCAN_AREA_SIZE - 40],
});
return (
<Modal visible={visible} animationType="slide" transparent={false}>
<CameraView
// eslint-disable-next-line react-native/no-inline-styles
style={{flex: 1}}
ref={cameraRef}
enableTorch={flash === 'on'}
barcodeScannerSettings={{
barcodeTypes: ['qr'],
}}
onBarcodeScanned={res => {
onBarcodeScanned(res.data);
}}>
{/* Overlay for dimming outside scan area */}
<View style={styles.overlay}>
<View style={[styles.topOverlay, {height: scanPositionTop + -0.8}]} />
<View
style={[styles.bottomOverlay, {height: scanPositionTop + -1.2}]}
/>
<View
style={[
styles.leftOverlay,
{
top: scanPositionTop - 1,
height: SCAN_AREA_SIZE + 2,
width: scanPositionLeft + 1,
},
]}
/>
<View
style={[
styles.rightOverlay,
{
top: scanPositionTop - 1,
height: SCAN_AREA_SIZE + 2,
width: scanPositionLeft + 1,
},
]}
/>
<View
style={[
styles.scanArea,
{top: scanPositionTop, left: scanPositionLeft},
]}>
{/* Corner borders for scan area */}
<View style={[styles.corner, styles.topLeft]} />
<View style={[styles.corner, styles.topRight]} />
<View style={[styles.corner, styles.bottomLeft]} />
<View style={[styles.corner, styles.bottomRight]} />
{/* Animated scan line */}
<Animated.View
style={[
styles.scanLine,
{transform: [{translateY: scanLineTranslate}]},
]}
/>
</View>
</View>
{/* Instructions */}
<View style={styles.instructionsContainer}>
<Text style={styles.instructions}>Scan QR Code</Text>
</View>
{/* Controls */}
<View style={styles.controls}>
<TouchableOpacity
style={styles.button}
onPress={() => setFlash(flash === 'off' ? 'on' : 'off')}>
<IonicIcons
name={flash === 'off' ? 'flash-outline' : 'flash'}
size={24}
color={'white'}
/>
<Text style={styles.buttonText}>Flash</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={onClose}>
<IonicIcons name="close-outline" size={24} color={'white'} />
<Text style={styles.buttonText}>Close</Text>
</TouchableOpacity>
</View>
</CameraView>
</Modal>
);
}
const SCAN_AREA_SIZE = 280;
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
},
topOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0,0,0,0.6)',
},
bottomOverlay: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0,0,0,0.6)',
},
leftOverlay: {
position: 'absolute',
left: 0,
backgroundColor: 'rgba(0,0,0,0.6)',
},
rightOverlay: {
position: 'absolute',
right: 0,
backgroundColor: 'rgba(0,0,0,0.6)',
},
scanArea: {
position: 'absolute',
width: SCAN_AREA_SIZE,
height: SCAN_AREA_SIZE,
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
},
corner: {
position: 'absolute',
width: 30,
height: 30,
borderColor: 'white',
},
topLeft: {
top: 0,
left: 0,
borderTopWidth: 4,
borderLeftWidth: 4,
},
topRight: {
top: 0,
right: 0,
borderTopWidth: 4,
borderRightWidth: 4,
},
bottomLeft: {
bottom: 0,
left: 0,
borderBottomWidth: 4,
borderLeftWidth: 4,
},
bottomRight: {
bottom: 0,
right: 0,
borderBottomWidth: 4,
borderRightWidth: 4,
},
scanLine: {
position: 'absolute',
width: SCAN_AREA_SIZE - 40,
height: 2,
backgroundColor: 'white',
left: 20,
top: 20,
shadowColor: 'white',
shadowOffset: {width: 0, height: 0},
shadowOpacity: 0.8,
shadowRadius: 8,
elevation: 5,
},
instructionsContainer: {
position: 'absolute',
top: 40,
width: '100%',
alignItems: 'center',
},
instructions: {
color: 'white',
fontSize: 20,
fontWeight: 'bold',
textShadowColor: 'black',
textShadowRadius: 2,
},
controls: {
position: 'absolute',
bottom: 40,
flexDirection: 'row',
justifyContent: 'space-around',
width: '100%',
alignItems: 'center',
},
button: {
alignItems: 'center',
padding: 10,
borderRadius: 8,
},
buttonText: {
color: 'white',
fontSize: 14,
marginTop: 4,
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment