React Native Barcode Scanner Overlay: Custom UI with Real-Time Contours
A shelf holds six labels in one camera frame, the scanner returns six values, and the operator still cannot tell which value belongs to which label. A custom overlay closes that gap: Dynamsoft’s React Native SDK reports a four-point location.points quadrilateral for every decoded barcode, and your app turns those points into contour lines, corner markers and text callouts using ordinary React Native View components. The SDK ships no scanner screen, so the entire visual layer is yours.
The difficult part is not drawing the box — it is knowing which coordinate system the points are in. item.location.points are image coordinates in the camera frame (1080 × 1920 in portrait, 1920 × 1080 in landscape), while left/top on an absolutely positioned View are preview coordinates in density-independent pixels measured by onLayout. This guide starts with the recognition primitives, then gives the scale + crop-offset transform between the two systems, rotation handling, the contour component, and a measured multi-code overlay result from the sample.
What you’ll build: A headless React Native barcode scanner whose overlay draws green contours, red corner dots and text/format callouts over every barcode in a live frame — on Android and iOS, with React Native 0.79 and the New Architecture enabled.
Key Takeaways
dynamsoft-capture-vision-react-nativeis headless:CaptureVisionRouter.getInstance()+CameraEnhancer.getInstance()+addResultReceiver()deliverDecodedBarcodesResult.items, and the app owns every visible widget.- Each
BarcodeResultItemcarriestext,formatStringandlocation.points(four corners) in image space. Preview space is theCameraViewmeasured byonLayout; never mix the two. - Transform:
scale = max(viewW / imageW, viewH / imageH)for aspect-fill, thenx' = x * scale - (imageW * scale - viewW) / 2and the equivalent fory. The subtracted terms are the crop offsets. - Orientation and layout changes invalidate the camera image basis (1080 × 1920 ↔ 1920 × 1080) and the preview dimensions. Read both from refs updated by
useWindowDimensions()andonLayout; values captured in a once-registered receiver go stale. - Measured result: the sample’s overlay screenshot shows at least seven simultaneous quadrilaterals spanning
DATAMATRIXandEAN_13, with green contours, red corner dots and text callouts aligned to each label. - Verified stack: React Native 0.79.0 / React 19.0.0,
newArchEnabled=true,dynamsoft-capture-vision-react-native3.6.2000 anddynamsoft-barcode-reader-bundle-react-native11.6.2000 — both are the current npm releases as of 2026-11-12.
Common Developer Questions
How do I draw a contour around a detected barcode in React Native?
Use the four item.location.points corners returned by onDecodedBarcodesReceived, convert them into preview coordinates, and render four absolutely positioned View lines rotated with transform: [{ rotate: ... }] plus four small View circles for the corners. The BarcodeContour component in Step 3 is the working implementation.
How do I convert Dynamsoft image coordinates to React Native preview coordinates?
Compute scale = max(viewWidth / imageWidth, viewHeight / imageHeight), subtract cropOffsetX = (imageWidth * scale - viewWidth) / 2 and cropOffsetY = (imageHeight * scale - viewHeight) / 2 from each scaled point, then use the result as left/top. Use 1080 × 1920 as the image basis in portrait and 1920 × 1080 in landscape.
What is the difference between preview coordinates and image coordinates?
Image coordinates are pixels in the SDK’s camera frame (a fixed 1920 × 1080 sensor resolution, reported rotated in portrait); they describe where the barcode was found in the captured image. Preview coordinates are density-independent pixels inside the CameraView widget, obtained from onLayout, and describe where a View is drawn on screen. Because the preview is aspect-filled, parts of the image are cropped — that is why a scale alone is not enough and crop offsets are required.
How do I keep overlays aligned when the device rotates?
Use useWindowDimensions() instead of a one-time Dimensions.get('window') call, store the current orientation and onLayout size in refs, and read those refs inside the result receiver. A receiver registered once in useEffect otherwise keeps mount-time values and misplaces every box after rotation. Step 2 shows the exact pattern.
Why are barcode overlays offset on tablets or wide-aspect devices?
The crop-offset formula assumes the CameraView fills its parent with aspect-fill. If the view is constrained to a smaller region, pass the exact onLayout width and height rather than falling back to window dimensions, and recompute the offsets whenever that layout changes.
Does the Dynamsoft React Native SDK provide a built-in scanner UI?
No. CameraView renders only the camera feed; contours, callouts, buttons, themes and navigation are standard React Native components. Changing the overlay never touches recognition settings, and changing recognition settings never changes the UI.
Which React Native and SDK versions work with this overlay sample?
The sample builds on React Native 0.79.0 with React 19.0.0 and newArchEnabled=true. dynamsoft-capture-vision-react-native 3.6.2000 declares react-native >=0.71.0-rc.0 <1.0.x, and dynamsoft-barcode-reader-bundle-react-native 11.6.2000 requires Capture Vision >= 3.4.3010; both packages ship codegenConfig, so they work with the New Architecture.
Which Overlay Layer Do You Need?
| Goal | Use | Coordinate work |
|---|---|---|
| Highlight one barcode | First items[0] quad only |
Image → preview transform |
| Highlight every code in the frame | Map over the full items array |
Same transform per item |
| Show decoded text and symbology | A View at the quad centroid |
Transform + centroid |
| Keep alignment through rotation | useWindowDimensions() + layout refs |
Recompute image basis and offsets |
| Decode a still image instead | CaptureVisionRouter.capture() |
Image space only; no preview crop |
The Recognition Primitives (No UI Included)
Before any overlay code, this is the whole recognition path. The SDK is a data source; nothing here renders a button, a box or a theme.
import {
CameraEnhancer,
CameraView,
CaptureVisionRouter,
EnumPresetTemplate,
} from 'dynamsoft-capture-vision-react-native';
const cvr = CaptureVisionRouter.getInstance();
const camera = CameraEnhancer.getInstance();
cvr.setInput(camera);
camera.setCameraView(cameraView.current);
const receiver = cvr.addResultReceiver({
onDecodedBarcodesReceived: ({ items }) => {
// items is BarcodeResultItem[] — EVERY code decoded in this frame.
// item.text -> string
// item.formatString -> 'QR_CODE', 'DATAMATRIX', 'EAN_13', ...
// item.location.points -> 4 corners in IMAGE coordinates
},
});
camera.open();
cvr.startCapturing(EnumPresetTemplate.PT_READ_BARCODES);
// on unmount: cvr.removeResultReceiver(receiver); cvr.stopCapturing(); camera.close();
Note the RN SDK uses singleton accessors (getInstance()), unlike the instance-based 3.6.x Flutter API. The items array is the multi-code primitive: one frame can contain several codes of different symbologies, and each item supplies its own quadrilateral.
Image Coordinates vs. Preview Coordinates
| Image coordinates | Preview coordinates | |
|---|---|---|
| Source | item.location.points from the SDK |
onLayout width/height, left/top styles |
| Unit | Camera-frame pixels | Density-independent pixels (dp) |
| Portrait basis | 1080 × 1920 | viewWidth × viewHeight |
| Landscape basis | 1920 × 1080 | viewWidth × viewHeight |
| Origin | Top-left of the captured frame | Top-left of the CameraView |
| Aspect-fill crop | Not applied | Applied; must be compensated |
The camera stream is scaled with Math.max so it fills the view without distortion; the excess is cropped equally on both axes. A point therefore needs both a scale and a translation before it can be used as left/top.
Step 1: Convert Image Points to Preview Points
const { width: viewWidth, height: viewHeight } = viewRef.current;
const isLandscape = orientationRef.current === 'landscape';
const imageWidth = isLandscape ? 1920 : 1080;
const imageHeight = isLandscape ? 1080 : 1920;
const scale = Math.max(viewWidth / imageWidth, viewHeight / imageHeight);
const cropOffsetX = (imageWidth * scale - viewWidth) / 2;
const cropOffsetY = (imageHeight * scale - viewHeight) / 2;
const overlays: BarcodeOverlay[] = items.map(item => {
const scaledCorners = item.location.points.map(point => ({
x: point.x * scale - cropOffsetX,
y: point.y * scale - cropOffsetY,
}));
const centerX = scaledCorners.reduce((sum, p) => sum + p.x, 0) / 4;
const centerY = scaledCorners.reduce((sum, p) => sum + p.y, 0) / 4;
const stableId = `${item.text}-${item.formatString}-${Math.round(centerX / 10) * 10}-${Math.round(centerY / 10) * 10}`;
return {
id: stableId,
text: item.text,
formatString: item.formatString,
x: centerX,
y: centerY,
corners: scaledCorners,
};
});
setBarcodeOverlays(overlays);
Math.max preserves the aspect ratio and fills the view (the same behaviour as a resizeMode="cover" image). cropOffsetX/cropOffsetY remove the invisible part of the scaled frame. The stable ID keeps React keys unchanged between consecutive frames so contours do not flicker.
interface BarcodeOverlay {
id: string;
text: string;
formatString: string;
x: number;
y: number;
corners: Array<{ x: number, y: number }>;
}
Step 2: Keep the Transform Correct Across Rotations
A result receiver is registered once, so values captured in its closure become stale. useWindowDimensions() re-renders on rotation; store the live orientation and the live CameraView size in refs and read them inside the receiver.
import { useWindowDimensions } from 'react-native';
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const viewRef = useRef({ width: windowWidth, height: windowHeight });
const orientationRef = useRef<'portrait' | 'landscape'>(
windowWidth > windowHeight ? 'landscape' : 'portrait',
);
useEffect(() => {
orientationRef.current = windowWidth > windowHeight ? 'landscape' : 'portrait';
viewRef.current = { width: windowWidth, height: windowHeight };
}, [windowWidth, windowHeight]);
// Inside the JSX:
<CameraView
style={StyleSheet.absoluteFill}
ref={cameraView}
onLayout={(event) => {
const { width, height } = event.nativeEvent.layout;
viewRef.current = { width, height };
}}
/>
Using a single Dimensions.get('window') call at mount time, or a useState value read inside the receiver, leaves the overlay using the old 1080 × 1920 basis after the phone turns — every box lands in the wrong place. Re-running the transform on orientation change also handles split-screen and foldable layout changes.
Step 3: Draw Contours and Corner Markers
Four rotated View lines form the contour; four small circles mark the corners.
const BarcodeContour: React.FC<{ corners: Array<{ x: number, y: number }> }> = ({ corners }) => {
if (corners.length !== 4) {
return null;
}
return (
<>
{corners.map((corner, index) => {
const nextCorner = corners[(index + 1) % 4];
const lineWidth = 3;
const deltaX = nextCorner.x - corner.x;
const deltaY = nextCorner.y - corner.y;
const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
return (
<View
key={index}
style={[
styles.contourLine,
{
left: corner.x,
top: corner.y - lineWidth / 2,
width: length,
height: lineWidth,
transform: [{ rotate: `${angle}deg` }],
},
]}
/>
);
})}
{corners.map((corner, index) => (
<View
key={`dot-${index}`}
style={[
styles.cornerDot,
{
left: corner.x - 4,
top: corner.y - 4,
},
]}
/>
))}
</>
);
};
Step 4: Add Text Callouts and Expire Stale Overlays
return (
<SafeAreaView style={StyleSheet.absoluteFill}>
<CameraView
style={StyleSheet.absoluteFill}
ref={cameraView}
onLayout={(event) => {
const { width, height } = event.nativeEvent.layout;
viewRef.current = { width, height };
}}
/>
{barcodeOverlays.map((overlay) => (
<BarcodeContour key={`contour-${overlay.id}`} corners={overlay.corners} />
))}
{barcodeOverlays.map((overlay) => (
<View
key={overlay.id}
style={[
styles.barcodeOverlay,
{
left: overlay.x - 75,
top: overlay.y - 40,
},
]}
>
<Text style={styles.barcodeText}>{overlay.text}</Text>
<Text style={styles.formatText}>{overlay.formatString}</Text>
</View>
))}
</SafeAreaView>
);
Live frames repeat the same code many times. Clear the overlay layer after a short quiet period so a box does not linger after the label leaves the frame:
if (clearTimeoutRef.current) {
clearTimeout(clearTimeoutRef.current);
}
clearTimeoutRef.current = setTimeout(() => setBarcodeOverlays([]), 2000);
On unmount, remove the receiver and release the camera:
return () => {
sub.remove();
cvr.stopCapturing();
camera.close();
cvr.removeResultReceiver(receiver);
if (clearTimeoutRef.current) {
clearTimeout(clearTimeoutRef.current);
}
};
Step 5: Style the Overlay Layer
const styles = StyleSheet.create({
barcodeOverlay: {
position: 'absolute',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderRadius: 8,
padding: 8,
alignItems: 'center',
minWidth: 150,
maxWidth: 250,
},
barcodeText: {
color: 'white',
fontSize: 14,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 2,
},
formatText: {
color: '#00FF00',
fontSize: 10,
textAlign: 'center',
},
contourLine: {
position: 'absolute',
backgroundColor: '#00FF00',
transformOrigin: 'left center',
},
cornerDot: {
position: 'absolute',
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: '#FF0000',
},
});
Measured Overlay Result
The screenshot below is the working overlay from the sample app: green contours, red corner dots, white decoded text and green symbology labels, all drawn over a live camera frame containing many codes at once.

Visible callouts in that single frame include:
| Decoded text (visible callout) | Symbology label |
|---|---|
BRP.CR0006M.CO.14/12.A.2.DS2 |
DATAMATRIX |
BRP.CR0002M.CO.14/12.A.2.DS2 |
DATAMATRIX |
BRP.CR0001M.CO.14/12.A.2.DS2 |
DATAMATRIX |
4021404036699 |
EAN_13 |
021404036699 |
DATAMATRIX |
Z014391851FFF0000212916814 |
DATAMATRIX |
At least seven quadrilaterals are drawn simultaneously across two symbology families, which is the multi-code behaviour the items array enables. The contours track the printed labels rather than sitting at fixed screen positions — that alignment is the result of the transform in Step 1, not of any SDK-provided UI.
Watch the Final Result: Real-Time Barcode Scanner Overlay in Action
Tested Environment
| Item | Value |
|---|---|
| React Native / React | 0.79.0 / 19.0.0 |
| New Architecture | newArchEnabled=true, Hermes enabled |
dynamsoft-capture-vision-react-native |
3.6.2000 — latest on npm, checked 2026-11-12 |
dynamsoft-barcode-reader-bundle-react-native |
11.6.2000 — latest on npm, checked 2026-11-12 |
| Peer ranges | React >= 16.8.1, RN >= 0.71.0-rc.0 < 1.0.x; DBR bundle requires Capture Vision >= 3.4.3010; both packages publish codegenConfig |
| Android toolchain | Gradle 8.10.2, Java 17, compileSdk 35, minSdk 24, targetSdk 34, NDK 27.1.12297006, Kotlin 2.0.21 |
| CI evidence | sample CI run 42550773700 — Android and iOS simulator builds passed for react-native-barcode-scanner |
| Local checks (2026-11-12) | npx eslint src/App.tsx clean; npx tsc --noEmit reports two pre-existing type errors inside the SDK package’s own TypeScript sources, none in the app code; ./gradlew assembleDebug --no-daemon → BUILD SUCCESSFUL, debug APK 196,666,060 bytes, built with the domestic Gradle mirror |
| Not verified | Overlay alignment after rotation on a physical device; iOS device run |
CI builds prove the project compiles on both platforms; they do not confirm that the contours line up with the printed labels on a real camera feed. The overlay screenshot above is a captured device result, while the rotation fix is verified by static analysis and build, not by a new device run.
Prerequisites
- React Native development environment set up (React Native CLI setup guide), with Node >= 18 and a physical Android or iOS device for live camera testing.
- Dynamsoft Barcode Reader license - Get your 30-day free trial license and replace
Licenseinsrc/App.tsx. The online trial key needs network access on first activation.
Start from the sample rather than scaffolding a scanner from scratch:
git clone https://github.com/yushulx/android-camera-barcode-mrz-document-scanner.git
cd android-camera-barcode-mrz-document-scanner/examples/react-native-barcode-scanner
npm install --legacy-peer-deps
npx react-native run-android # or run-ios
Common Issues & Edge Cases
- Overlays misaligned on tablets or wide-aspect devices: The crop-offset formula assumes the
CameraViewfills its parent with aspect-fill. Constrain the view to a known region, pass its exactonLayoutdimensions, and recompute offsets whenever that layout changes. - Boxes jump to the wrong place after rotating the phone: A once-registered result receiver captures mount-time dimensions. Read orientation and view size from refs updated by
useWindowDimensions()andonLayout, and swap the image basis between 1080 × 1920 and 1920 × 1080. - Points seem rotated by 90° even before scaling:
location.pointsare camera-frame coordinates, and the SDK reports the rotated basis in portrait. Do not apply an extra rotation on top; use the portrait/landscape image dimensions shown in Step 1. If your device reports a different sensor resolution, read it from the camera configuration rather than assuming 1920 × 1080. - No overlays appear on iOS: Grant camera permission in
Info.plist(NSCameraUsageDescription) and initialize the license beforestartCapturing(). A missing, expired or unreachable online trial license silently prevents result callbacks. - Overlays flicker or lag on low-end Android devices: Debounce
setBarcodeOverlays, keep stable IDs so React does not remount contour views each frame, and consider limiting tracked results inSimplifiedBarcodeReaderSettings. - Contours linger after the label leaves the frame: The 2-second clear timer only fires when no new results arrive. Reset it on every callback and clear it on unmount.
- Windows build fails with
Filename longer than 260 charactersinComponentDescriptors.cpp: React Native New Architecture codegen creates deeply nested paths undernode_modules. Build the project from a short root path (for exampleD:/rn/) or enable Windows long paths; the error is a filesystem limit, not an SDK or overlay problem. - Camera stays black on Android: Ensure the runtime camera permission is granted and that
camera.open()andcvr.startCapturing()both run; wrap the latter in a.catch()and surface the error instead of leaving a blank preview.
Conclusion
A good overlay is a coordinate problem, not a drawing problem. The Dynamsoft React Native SDK stays headless and hands you items with four image-space corners per barcode; your app scales those corners into the CameraView measured by onLayout, compensates for aspect-fill cropping, keeps refs current across rotations, and draws contours and callouts with plain View components. Because the recognition layer and the UI layer are separate, the same transform works for a single QR code, a shelf full of DataMatrix labels, or a mixed 1D/2D frame — and the whole look and feel stays under developer control.