Components Medium
Expo Camera
A camera component using expo-camera with photo capture, front/back toggle, flash control, and captured photo preview with retake option.
react-native typescript expo-camera
Targets: React Native
Expo Snack
Code
import React, { useRef, useState } from "react";
import { Image, Pressable, StyleSheet, Text, View } from "react-native";
import { CameraView, useCameraPermissions, CameraType, FlashMode } from "expo-camera";
/* ------------------------------------------------------------------ */
/* CameraCapture */
/* ------------------------------------------------------------------ */
interface CameraCaptureProps {
onPhotoTaken?: (uri: string) => void;
facing?: CameraType;
flashEnabled?: boolean;
}
function CameraCapture({
onPhotoTaken,
facing: initialFacing = "back",
flashEnabled: initialFlash = false,
}: CameraCaptureProps) {
const [permission, requestPermission] = useCameraPermissions();
const [facing, setFacing] = useState<CameraType>(initialFacing);
const [flash, setFlash] = useState<boolean>(initialFlash);
const [photoUri, setPhotoUri] = useState<string | null>(null);
const cameraRef = useRef<CameraView>(null);
/* ---- Permission screen ---- */
if (!permission) {
return <View style={styles.container} />;
}
if (!permission.granted) {
return (
<View style={styles.permissionContainer}>
<Text style={styles.permissionTitle}>Camera Access</Text>
<Text style={styles.permissionText}>
This app needs access to your camera to take photos.
</Text>
<Pressable style={styles.permissionButton} onPress={requestPermission}>
<Text style={styles.permissionButtonText}>Grant Permission</Text>
</Pressable>
</View>
);
}
/* ---- Photo preview ---- */
if (photoUri) {
return (
<View style={styles.container}>
<Image source={{ uri: photoUri }} style={styles.preview} />
<View style={styles.previewActions}>
<Pressable
style={[styles.previewButton, styles.retakeButton]}
onPress={() => setPhotoUri(null)}
>
<Text style={styles.previewButtonText}>Retake</Text>
</Pressable>
<Pressable
style={[styles.previewButton, styles.useButton]}
onPress={() => onPhotoTaken?.(photoUri)}
>
<Text style={styles.previewButtonText}>Use Photo</Text>
</Pressable>
</View>
</View>
);
}
/* ---- Camera view ---- */
const toggleFacing = () => setFacing((prev) => (prev === "back" ? "front" : "back"));
const toggleFlash = () => setFlash((prev) => !prev);
const capture = async () => {
if (!cameraRef.current) return;
const photo = await cameraRef.current.takePictureAsync();
if (photo) setPhotoUri(photo.uri);
};
return (
<View style={styles.container}>
<CameraView
ref={cameraRef}
style={styles.camera}
facing={facing}
flash={flash ? "on" : "off"}
/>
{/* Top bar */}
<View style={styles.topBar}>
<Pressable style={styles.iconButton} onPress={toggleFlash}>
<Text style={styles.iconText}>{flash ? "⚡" : "⚡\u0336"}</Text>
</Pressable>
</View>
{/* Bottom controls */}
<View style={styles.bottomBar}>
<View style={styles.bottomSide} />
{/* Capture button */}
<Pressable style={styles.captureOuter} onPress={capture}>
<View style={styles.captureInner} />
</Pressable>
{/* Flip camera */}
<View style={styles.bottomSide}>
<Pressable style={styles.iconButton} onPress={toggleFacing}>
<Text style={styles.iconText}>🔄</Text>
</Pressable>
</View>
</View>
</View>
);
}
/* ------------------------------------------------------------------ */
/* Styles */
/* ------------------------------------------------------------------ */
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0f172a",
},
/* Permission */
permissionContainer: {
flex: 1,
backgroundColor: "#0f172a",
justifyContent: "center",
alignItems: "center",
padding: 32,
},
permissionTitle: {
color: "#f8fafc",
fontSize: 24,
fontWeight: "700",
marginBottom: 12,
},
permissionText: {
color: "#94a3b8",
fontSize: 16,
textAlign: "center",
marginBottom: 24,
lineHeight: 24,
},
permissionButton: {
backgroundColor: "#6366f1",
paddingHorizontal: 32,
paddingVertical: 14,
borderRadius: 12,
},
permissionButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
/* Camera */
camera: {
...StyleSheet.absoluteFillObject,
},
topBar: {
position: "absolute",
top: 60,
left: 0,
right: 0,
flexDirection: "row",
justifyContent: "flex-end",
paddingHorizontal: 20,
},
bottomBar: {
position: "absolute",
bottom: 40,
left: 0,
right: 0,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 32,
},
bottomSide: {
width: 48,
alignItems: "center",
},
iconButton: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: "rgba(0,0,0,0.45)",
justifyContent: "center",
alignItems: "center",
},
iconText: {
fontSize: 22,
},
captureOuter: {
width: 76,
height: 76,
borderRadius: 38,
borderWidth: 4,
borderColor: "#fff",
justifyContent: "center",
alignItems: "center",
},
captureInner: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: "#fff",
},
/* Preview */
preview: {
flex: 1,
},
previewActions: {
position: "absolute",
bottom: 40,
left: 0,
right: 0,
flexDirection: "row",
justifyContent: "center",
gap: 16,
},
previewButton: {
paddingHorizontal: 28,
paddingVertical: 14,
borderRadius: 12,
},
retakeButton: {
backgroundColor: "rgba(255,255,255,0.15)",
},
useButton: {
backgroundColor: "#6366f1",
},
previewButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
});
/* ------------------------------------------------------------------ */
/* Demo App */
/* ------------------------------------------------------------------ */
export default function App() {
const [lastPhoto, setLastPhoto] = useState<string | null>(null);
if (lastPhoto) {
return (
<View style={styles.container}>
<Image source={{ uri: lastPhoto }} style={{ flex: 1 }} resizeMode="contain" />
<Pressable
style={{
position: "absolute",
bottom: 50,
alignSelf: "center",
backgroundColor: "#6366f1",
paddingHorizontal: 28,
paddingVertical: 14,
borderRadius: 12,
}}
onPress={() => setLastPhoto(null)}
>
<Text style={{ color: "#fff", fontSize: 16, fontWeight: "600" }}>Back to Camera</Text>
</Pressable>
</View>
);
}
return <CameraCapture onPhotoTaken={(uri) => setLastPhoto(uri)} />;
}Expo Camera
A full-featured camera component built with expo-camera that provides photo capture, front/back camera toggle, flash control, and a captured photo preview with retake functionality. Designed for seamless integration into any Expo project.
Features
- Photo capture — Large circular shutter button for intuitive photo taking.
- Front/back toggle — Switch between front and rear cameras with a single tap.
- Flash control — Toggle flash on or off before capturing.
- Permission handling — Graceful permission request flow using
useCameraPermissions. - Photo preview — After capture, review the photo with options to retake or confirm.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
onPhotoTaken | (uri: string) => void | — | Callback fired when the user confirms a captured photo. |
facing | "front" | "back" | "back" | Initial camera facing direction. |
flashEnabled | boolean | false | Whether flash is enabled by default. |
Usage
import CameraCapture from "./CameraCapture";
<CameraCapture onPhotoTaken={(uri) => console.log("Photo URI:", uri)} />
How it works
- The component first checks camera permissions via
useCameraPermissions. If not granted, a permission request screen is displayed. - Once permissions are granted, the
CameraViewfromexpo-camerarenders a live camera preview filling the screen. - Toolbar buttons at the bottom allow toggling flash and switching between front/back cameras.
- Pressing the capture button takes a photo using the camera ref’s
takePictureAsyncmethod. - The captured photo is displayed in a full-screen preview with “Retake” (returns to camera) and “Use Photo” (triggers the
onPhotoTakencallback) buttons.