modern-barcode-scanner
v2.0.0
Published
A high-performance React barcode scanner with an owned WebAssembly engine and capability-aware camera controls
Maintainers
Readme
Interface Preview

Desktop idle state at 1440 × 900. The same interface is audited across desktop, tablet, and mobile in the design audit.
✨ Features
- 🚀 High Performance: A first-party ZXing-C++ WebAssembly engine prioritizes the visible scan region, scores frame quality off-thread, coalesces stale queued work, reuses memory, and copies only luminance into WASM.
- 📱 Mobile Optimized: Responsive camera constraints without mandatory zoom or focus settings.
- 🔦 Torch Control: Shown only when the active camera reports torch support.
- 🔄 Camera Switching: Shown only when multiple video inputs are available.
- 🎯 Session Management: Prevents stale results with session-based tracking.
- 🎨 Customizable UI: CSS-based styling with sensible defaults and CSS variables.
- ♿ Accessible Controls: Keyboard focus states, reduced-motion support, live scanner status, and mobile-safe touch targets.
- 📦 TypeScript Support: Full type definitions included out of the box.
- 📳 Haptic Feedback: Standard Web Vibration API support for successful scans where
navigator.vibrateis available. - 🔊 Sound Feedback: Optional audio cues on successful scans.
🏷️ Supported Barcode Formats
- 2D Codes: QR Code, Micro QR, rMQR, Data Matrix, PDF417/MicroPDF417, Aztec, MaxiCode
- Retail Codes: EAN-13, EAN-8, UPC-A, UPC-E, including valid 2- and 5-digit retail supplements
- Industrial/Standard Codes: Code 128, Code 39, Code 93, Codabar, ITF (Interleaved 2 of 5)
- Books: Bookland ISBN-13
- DataBar (GS1)
- Additional: Telepen, Code 32, DX Film Edge, and more supported by the pinned ZXing-C++ reader build
📦 Installation
Choose your preferred package manager:
# npm
npm install modern-barcode-scanner
# yarn
yarn add modern-barcode-scanner
# pnpm
pnpm add modern-barcode-scanner🚀 Quick Start
Here's a minimal example to get the scanner up and running in your React application:
import { useRef, useEffect } from "react";
import { BarcodeScanner } from "modern-barcode-scanner";
import type { BarcodeScannerRef, ScanResult } from "modern-barcode-scanner";
// Import the stylesheet once, anywhere in your app. The CSS ships as a separate
// file (so you can override the design tokens), so it is NOT injected
// automatically — this import is required for the scanner to look right.
import "modern-barcode-scanner/styles.css";
function App() {
const scannerRef = useRef<BarcodeScannerRef>(null);
const handleScan = (result: ScanResult) => {
console.log("📦 Barcode type:", result.typeName);
console.log("📄 Barcode data:", result.scanData);
// Scanner automatically stops after detection.
// Call scannerRef.current?.start() to scan again!
};
const handleError = (error: Error) => {
console.error("❌ Scanner error:", error.message);
};
useEffect(() => {
// Start scanning when component mounts
scannerRef.current?.start();
}, []);
return (
<div style={{ width: "100vw", height: "100vh" }}>
<BarcodeScanner
ref={scannerRef}
onScan={handleScan}
onError={handleError}
themeColor="#2563EB" // Customize the primary UI color!
/>
</div>
);
}Zero bundler configuration
Under the hood, this library uses its own pinned, reader-only ZXing-C++ WebAssembly build behind a modular decoder boundary. Both the worker and its WebAssembly binary are inlined directly into the bundle — the worker as a Blob and the .wasm inside a single-file Emscripten module.
This means the published package does not require a separately hosted worker or .wasm asset: install it, import it, and include its stylesheet. The production package path is exercised end to end in the browser test matrix. Applications with unusual bundle transforms or a restrictive Content Security Policy should retain the package's Blob worker and run the browser tests described below.
The trade-off is a larger main bundle (the WASM binary is embedded), in exchange for it working out of the box in supported consumers with no hosted engine assets.
The source lock, reproducible build, artifact checksums, SBOM, test matrix, and smart-enhancement roadmap are documented in the first-party decoder migration. Responsive states, visual decisions, and the current Hallmark review are recorded in the design audit. No @undecaf/zbar-wasm runtime dependency remains.
📖 API Reference
<BarcodeScanner /> Component
Props
| Prop | Type | Default | Description |
| ------------------- | ------------------------------- | --------------- | ---------------------------------------------------------- |
| onScan | (result: ScanResult) => void | Required | Callback fired when a barcode is detected. |
| onError | (error: Error) => void | undefined | Callback fired when an error occurs. |
| onStateChange | (state: ScannerState) => void | undefined | Callback fired when scanner state changes. |
| themeColor | string | '#2563EB' | Primary color for the viewfinder, scan line, and controls. |
| scanInterval | number | 100 | Time between scan attempts (in ms). |
| enableVibration | boolean | true | Enable haptic feedback on scan (uses navigator.vibrate). |
| vibrationDuration | number | 200 | Vibration duration (in ms). |
| enableSound | boolean | false | Enable sound feedback on scan. |
| initialFacingMode | 'user' \| 'environment' | 'environment' | Initial camera to use. |
| showScanLine | boolean | true | Show scanning animation line. |
| showCameraSwitch | boolean | true | Show camera switch button. |
| showTorchButton | boolean | true | Show torch button (if supported). |
| className | string | '' | Custom CSS class for the container. |
| style | React.CSSProperties | undefined | Custom inline styles for the container. |
Ref Methods
Exposed via useImperativeHandle for direct control:
interface BarcodeScannerRef {
start: () => Promise<void>; // Starts the camera and scanning
stop: () => void; // Stops the camera and scanning
switchCamera: () => Promise<void>; // Toggles between front and back camera
toggleTorch: () => Promise<void>; // Toggles the torch/flash (if supported)
getState: () => ScannerState; // Returns current state
}TypeScript Types
interface ScanResult {
typeName: string; // e.g., 'QRCODE', 'EAN13', 'CODE128'
scanData: string; // The decoded barcode string
}
interface ScannerState {
isStarting: boolean;
isScanning: boolean;
facingMode: "user" | "environment";
isTorchOn: boolean;
isTorchSupported: boolean;
canSwitchCamera: boolean;
}🛠️ Advanced Usage
Using the useScanner Hook
If you need complete control over the UI, you can use the exported hook directly:
import {
IconCamera,
IconCameraOff,
IconRotateCamera,
IconTorchOff,
IconTorchOn,
useScanner,
} from "modern-barcode-scanner";
function CustomScanner() {
const {
scannerState,
videoRef,
canvasRef,
viewfinderRef,
handleScan,
handleStopScan,
handleSwitchCamera,
handleToggleTorch,
} = useScanner({
onScan: (result) => console.log("Scanned:", result),
onError: (error) => console.error("Error:", error),
enableVibration: true,
});
return (
<div style={{ position: "relative", width: "100%", height: 480 }}>
<video
ref={videoRef}
autoPlay
muted
playsInline
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
<div
ref={viewfinderRef}
style={{ position: "absolute", inset: "25% 10%", border: "2px solid #2563eb" }}
>
Align the barcode here
</div>
<canvas ref={canvasRef} hidden />
<div className="controls">
<button type="button" onClick={handleScan}>
<IconCamera /> Start
</button>
<button type="button" onClick={handleStopScan}>
<IconCameraOff /> Stop
</button>
{scannerState.canSwitchCamera && (
<button type="button" onClick={handleSwitchCamera}>
<IconRotateCamera /> Switch camera
</button>
)}
{scannerState.isTorchSupported && (
<button type="button" onClick={handleToggleTorch}>
{scannerState.isTorchOn ? <IconTorchOff /> : <IconTorchOn />}
{scannerState.isTorchOn ? "Turn off torch" : "Turn on torch"}
</button>
)}
</div>
</div>
);
}Attach viewfinderRef to the region represented by your custom guide. The hook maps that displayed rectangle through the camera video's object-fit: cover crop and prioritizes it for decoding; when no measurable guide is attached, it safely falls back to full-frame scanning.
For custom interfaces, the package also exports ScannerControls, ScanLine, and the complete 24 px icon system: IconCamera, IconCameraOff, IconCameraPlaceholder, IconRotateCamera, IconTorchOn, IconTorchOff, IconScanFrame, IconCheck, IconAlert, and IconAdjustments. IconCameraPlaceholder remains a backward-compatible alias of the canonical IconScanFrame logo. Every icon inherits currentColor and accepts standard React SVG props.
Helper Utilities
The library exports several useful utilities:
import { isPhone, getBestRearCamera, getMediaConstraints } from "modern-barcode-scanner";
// 📱 Check if device is a phone/tablet
const isMobile = isPhone();
// 📷 Get a heuristically preferred rear camera device ID
const cameraId = await getBestRearCamera();
// ⚙️ Get optimized media constraints based on facing mode
const constraints = await getMediaConstraints("environment");getBestRearCamera() requests camera permission and probes available video inputs to identify a heuristically preferred rear camera. Camera labels and capabilities vary by browser and device, and the helper can open each camera briefly, so it remains an explicit advanced utility rather than part of the default startup path. getMediaConstraints() uses non-mandatory facing and resolution preferences for faster, more resilient startup.
🎨 Styling
The component uses CSS prefix mbs- (Modern Barcode Scanner) and component-scoped CSS variables for easy theming. It fills the dimensions of its parent, so give the parent an explicit height when embedding it in a page.
<div style={{ width: "100%", height: 480 }}>
<BarcodeScanner onScan={handleScan} />
</div>The scanner establishes its own CSS size container, so the viewfinder responds to the component's actual host rather than the browser viewport. This keeps embedded 250–480 px scanners balanced inside wide desktop layouts while retaining viewport-media fallbacks for older browsers.
CSS Variables
Override tokens on a scanner instance (or use the themeColor prop for the primary accent):
.my-scanner {
--mbs-primary: #ff0055;
--mbs-scan-color: #ff0055;
--mbs-bg: #fff8fb;
--mbs-bg-secondary: #ffeef5;
--mbs-control-bg: rgba(20, 10, 16, 0.68);
}Choose accent and background colors with sufficient contrast for your application, especially when overriding the defaults. Independent dark and light cue rails keep the viewfinder brackets and scan beam identifiable over both bright phone displays and dark camera feeds, including black or white custom accents.
| Token | Default | Purpose |
| ---------------------- | ----------------------------- | ------------------------------------------ |
| --mbs-primary | oklch(53% 0.21 256) | Viewfinder brackets and primary accent |
| --mbs-scan-color | var(--mbs-primary) | Animated scan line and restrained trail |
| --mbs-bg | oklch(98.5% 0.004 250) | Scanner background |
| --mbs-bg-secondary | oklch(95.5% 0.012 250) | Background highlight |
| --mbs-bg-card | oklch(99% 0.004 250 / 0.95) | Raised scanner surface |
| --mbs-text | oklch(24% 0.02 258) | Primary text |
| --mbs-text-secondary | oklch(45% 0.018 257) | Secondary text |
| --mbs-control-bg | oklch(18% 0.018 258 / 0.82) | Camera-control toolbar background |
| --mbs-control-opaque | oklch(18% 0.018 258) | Opaque camera hint on constrained layouts |
| --mbs-control-hover | oklch(95% 0.008 250 / 0.18) | Camera-control hover background |
| --mbs-border | oklch(53% 0.21 256 / 0.2) | Subtle borders and placeholder glow |
| --mbs-on-dark | oklch(96% 0.008 250) | Text and icons on camera surfaces |
| --mbs-scrim | oklch(12% 0.014 258 / 0.18) | Area outside the prioritized viewfinder |
| --mbs-cue-dark | oklch(8% 0.01 258 / 0.82) | Dark safety rail around the scanning cue |
| --mbs-cue-light | oklch(98% 0.004 250 / 0.9) | Light safety rail around the scanning cue |
| --mbs-radius-control | 0.375rem | Button and hint corner radius |
| --mbs-radius-frame | 0.875rem | Viewfinder and control-group corner radius |
Overriding Classes
/* Custom container styling */
.mbs-container {
border-radius: 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Custom scan line */
.mbs-scan-line {
height: 3px;
box-shadow: 0 0 15px 3px var(--mbs-primary);
}
/* Custom control buttons */
.mbs-control-btn {
background-color: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(4px);
}🌐 Browser Requirements
The scanner targets current Chrome, Edge, Firefox, and Safari releases on desktop and mobile. A browser must provide:
navigator.mediaDevices.getUserMediafor camera access.- Web Workers, WebAssembly, Canvas, and
requestAnimationFrame. - A secure context. Use HTTPS in production (
localhostis allowed for local development).
If your application enforces Content Security Policy, allow the package's self-contained Blob worker (normally worker-src 'self' blob:). Depending on browser and policy version, WebAssembly compilation may also require script-src 'wasm-unsafe-eval' or the broader legacy fallback 'unsafe-eval'; start with the narrower directive and verify your supported browsers. The scanner does not use JavaScript eval, external worker files, or cross-origin .wasm fetches. When embedded in an iframe, grant camera access with allow="camera" and an appropriate Permissions-Policy response header.
Camera switching, torch, vibration, and audio feedback depend on device and browser support. Unsupported optional features degrade gracefully; use onError to surface permission, camera, ended-stream, worker bootstrap/runtime/timeout, and torch failures to users. Camera dimensions are re-synchronized after metadata, orientation, resolution, and camera changes instead of relying on a startup snapshot.
The scanner processes frames locally in the browser and does not upload camera data.
The automated browser matrix exercises the Chromium, Firefox, and WebKit engines plus a Chromium fake-camera pipeline. Real mobile camera selection, torch behavior, autofocus, thermal limits, and permission UI still depend on physical hardware and should be included in an application's device acceptance testing.
⚡ Performance Optimizations
This library is built for speed and reliability:
- Web Worker Processing: Barcode detection runs entirely off the main thread.
- Adaptive Frame Quality: Uses a bounded, anti-aliasing luminance sample to score motion, blur, contrast, exposure, and glare before spending work on WASM decoding. Every third focused attempt remains an enhanced recovery pass, preventing a valid code on a bright phone screen from being filtered indefinitely.
- Viewfinder-First Detection: Scans the smaller guided region first and restores a complete, deeper full-frame pass every fifth attempt.
- Reusable WASM Frame Memory: Grows the decoder input allocation only when needed and reuses it across scans.
- Frame Throttling: Configurable
scanIntervalbalances detection latency with device battery and CPU usage. - Session Management: Monotonic sessions and exact request IDs prevent late or duplicate worker responses from unlocking a newer frame.
- Smart Downscaling: Bounds the captured frame to 1280 px on its longest side while preserving its aspect ratio, reducing work without distorting the symbol.
- Canvas Optimizations: Utilizes
willReadFrequentlyanddesynchronizedrendering hints where supported. - Failure Recovery: Readiness deadlines, bounded capture failures, worker response watchdogs, and camera-track lifecycle handling prevent silent scanning stalls.
- Defensive WASM Boundary: Both TypeScript and native code validate dimensions, byte lengths, heap ranges, engine results, and terminal decoder lifecycle state.
- Bounded Worker Backpressure: The worker accepts at most 1280 px per dimension and 1280² pixels, keeps at most one queued frame per scanner, and releases a superseded transferred buffer before decoding.
🧑💻 Development
This project uses Vite+ (vp) as its unified toolchain for Vite, Vitest, Oxlint, Oxfmt, Rolldown, tsdown, and Vite Task workflows. Development requires Node.js ^22.22.2, ^24.15.0, or >=26.0.0 and npm >=11.5.1. The npm scripts invoke the locally installed vp binary.
Dependency freshness was rechecked on 2026-08-23: the locked direct dependency set, Playwright browser bundle, ZXing-C++ 3.1.1 engine, and Emscripten 6.0.8 toolchain were current, npm outdated was empty, and the dependency audit reported no known vulnerabilities. React 18 and 19 remain the supported peer ranges even though development and verification use the current React 19 line.
# Install the exact locked dependency graph (includes Vite+)
npm ci
# Start the live demo app at http://localhost:8080
npm run devScripts
| Script | Description |
| -------------------------- | ------------------------------------------------------------------------------------------- |
| npm run dev | Run the demo app with hot-module reload. |
| npm run build | Build the library (ESM + CJS) and emit type declarations (tsc). |
| npm run preview | Preview a production build of the demo. |
| npm test | Run the test suite once (Vitest + jsdom + Testing Library). |
| npm run lint | Lint the code with Oxlint. |
| npm run format | Format the code with Oxfmt. |
| npm run check | Format check + lint + type-check in a single command. |
| npm run typecheck | Type-check the library and the demo with tsc. |
| npm run benchmark:engine | Run the repeatable warm focused/enhanced 392 × 392 QR engine benchmark. |
| npm run test:browser | Build, then test visual layouts, the worker, and a fake camera across browsers. |
| npm run engine:build | Rebuild the owned WASM engine from pinned source and toolchain inputs. |
| npm run engine:verify | Cross-check the native ABI, source/toolchain lock, SBOM, declaration, and artifact SHA-256. |
| npm run fixtures:browser | Regenerate deterministic QR fixtures used by the browser and fake-camera tests. |
Demo
The demo/ app consumes the library the same way a published consumer does — importing it by package name (modern-barcode-scanner) and stylesheet (modern-barcode-scanner/styles.css) against its public API. Aliases in demo/vite.config.ts resolve those entry points to the local build during development, keeping the demo live-reloading while validating the real package surface.
Append ?visual-audit to the local demo URL to open the camera-free core component audit page. It renders the real package CSS, the complete icon system, viewfinder variants, exported controls, capability-limited controls, and custom accents without requesting camera permission.
For deterministic responsive QA of the complete demo, use ?demo-state=<state>, where <state> is idle, starting, active, result, or error. These previews reuse the production components and styles without requiring camera hardware or a test barcode. The maintained viewport/state matrix and current findings are in docs/DESIGN_AUDIT.md.
Testing
Unit and integration tests live next to the source as *.test.ts(x) and run under Vitest (via vp test) in a jsdom environment, with @testing-library/react for component tests and independently generated real barcode fixtures for decoder tests. They include native ABI boundaries, malformed and detached buffers, worker backpressure and request coalescing, frame-quality/ROI behavior, retail supplements, camera lifecycle races, and engine recovery after damaged input. Playwright tests under tests/browser validate responsive desktop, portrait, compact, and short-landscape layouts; 250–480 px embedded hosts; 125–200% text sizing; rendered pixels in all eight corner arms; black/white accent extremes; hostile error strings; ultra-short result surfaces; dark, forced-colors, and reduced-motion preferences; keyboard focus containment; the production-style inline worker and transferable frame buffer in Chromium, Firefox, and WebKit; and the built public package through the complete Chromium fake-camera pipeline. Pull requests and pushes to dev or main repeat the static, dependency, fixed-platform reproducible-engine, unit, browser, build, and package gates in CI. Run the primary local suites with npm test and npm run test:browser, and track warm decoder performance with npm run benchmark:engine.
📝 License
MIT © Sumit Sahoo
Please refer to the LICENSE file for the project license and THIRD_PARTY_NOTICES.md for bundled decoder and generated-runtime notices.
🤝 Credits
- Barcode decoding powered by this repository's pinned, reader-only ZXing-C++ WebAssembly build.
- WebAssembly runtime generated with the pinned Emscripten 6.0.8 toolchain.
