@yudiel/react-qr-scanner
Version:
A library to scan QR codes in React.
673 lines (529 loc) • 20.7 kB
Markdown
# @yudiel/react-qr-scanner
[](https://www.npmjs.com/package/@yudiel/react-qr-scanner)
[](https://www.npmjs.com/package/@yudiel/react-qr-scanner)
[](https://github.com/yudielcurbelo/react-qr-scanner/blob/main/LICENSE)
A modern React library for scanning QR codes and barcodes using your device camera or webcam. Built on top of the
Barcode Detection API with React hooks and components.
## Features
- **Multiple Barcode Formats**: Supports QR codes, EAN, UPC, Code 128, and many more 1D/2D formats
- **Camera Controls**: Built-in torch (flashlight), zoom, and camera switching capabilities
- **Flexible Scanning**: Continuous scanning, single scan mode, or pause/resume functionality
- **Custom Tracking**: Draw custom overlays and tracking visualizations on detected barcodes
- **Device Selection**: Choose specific cameras with the `useDevices` hook
- **Customizable UI**: Custom styles, class names, and component overrides
- **Audio Feedback**: Optional beep sound on successful scans (with custom sound support)
- **TypeScript Support**: Fully typed for excellent developer experience
- **Lightweight**: Minimal dependencies with optimized bundle size
- **Cross-browser Compatible**: Works across modern browsers with `webrtc-adapter`
## Table of Contents
- [Demo](#demo)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Usage Examples](#usage-examples)
- [Basic Scanner](#basic-scanner)
- [Device Selection](#device-selection)
- [Camera Constraints](#camera-constraints)
- [Custom Tracking Overlay](#custom-tracking-overlay)
- [Pausing and Resuming](#pausing-and-resuming)
- [UI Components](#ui-components)
- [API Reference](#api-reference)
- [Scanner Props](#scanner-props)
- [Scanner Ref](#scanner-ref)
- [useDevices Hook](#usedevices-hook)
- [Utilities](#utilities)
- [Supported Formats](#supported-formats)
- [Type Definitions](#type-definitions)
- [Browser Support](#browser-support)
- [Troubleshooting](#troubleshooting)
- [Limitations](#limitations)
- [Contributing](#contributing)
- [License](#license)
## Demo
Check out the [live demo](https://yudielcurbelo.github.io/react-qr-scanner/) to see the scanner in action.
## Installation
```bash
npm install @yudiel/react-qr-scanner
```
```bash
yarn add @yudiel/react-qr-scanner
```
```bash
pnpm add @yudiel/react-qr-scanner
```
## Quick Start
```jsx
import { Scanner } from '@yudiel/react-qr-scanner';
function App() {
return (
<Scanner
onScan={(result) => console.log(result)}
onError={(error) => console.log(error?.message)}
/>
);
}
```
## Usage Examples
### Basic Scanner
```jsx
import { Scanner } from '@yudiel/react-qr-scanner';
function BasicExample() {
const handleScan = (detectedCodes) => {
console.log('Detected codes:', detectedCodes);
// detectedCodes is an array of IDetectedBarcode objects
detectedCodes.forEach(code => {
console.log(`Format: ${code.format}, Value: ${code.rawValue}`);
});
};
return (
<Scanner
onScan={handleScan}
onError={(error) => console.error(error)}
/>
);
}
```
### Device Selection
Use the `useDevices` hook to list available cameras and select a specific device:
```jsx
import { Scanner, useDevices } from '@yudiel/react-qr-scanner';
import { useState } from 'react';
function DeviceSelectionExample() {
const devices = useDevices();
const [selectedDevice, setSelectedDevice] = useState(null);
return (
<div>
<select onChange={(e) => setSelectedDevice(e.target.value)}>
<option value="">Select a camera</option>
{devices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label || `Camera ${device.deviceId}`}
</option>
))}
</select>
<Scanner
onScan={(result) => console.log(result)}
constraints={{
deviceId: selectedDevice,
}}
/>
</div>
);
}
```
### Camera Constraints
Customize camera settings using MediaTrackConstraints:
```jsx
import { Scanner } from '@yudiel/react-qr-scanner';
function ConstraintsExample() {
return (
<Scanner
onScan={(result) => console.log(result)}
constraints={{
facingMode: 'environment', // Use rear camera
aspectRatio: 1, // Square aspect ratio
// Advanced constraints
width: { ideal: 1920 },
height: { ideal: 1080 },
}}
/>
);
}
```
### Custom Tracking Overlay
Draw custom visualizations on detected barcodes:
```jsx
import { Scanner } from '@yudiel/react-qr-scanner';
function TrackingExample() {
const highlightCodeOnCanvas = (detectedCodes, ctx) => {
detectedCodes.forEach((detectedCode) => {
const { boundingBox, cornerPoints } = detectedCode;
// Draw bounding box
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 4;
ctx.strokeRect(
boundingBox.x,
boundingBox.y,
boundingBox.width,
boundingBox.height
);
// Draw corner points
ctx.fillStyle = '#FF0000';
cornerPoints.forEach((point) => {
ctx.beginPath();
ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI);
ctx.fill();
});
});
};
return (
<Scanner
onScan={(result) => console.log(result)}
components={{
tracker: highlightCodeOnCanvas,
}}
/>
);
}
```
### Pausing and Resuming
Control when the scanner is active:
```jsx
import { Scanner } from '@yudiel/react-qr-scanner';
import { useState } from 'react';
function PauseExample() {
const [isPaused, setIsPaused] = useState(false);
return (
<div>
<button onClick={() => setIsPaused(!isPaused)}>
{isPaused ? 'Resume' : 'Pause'} Scanning
</button>
<Scanner
onScan={(result) => console.log(result)}
paused={isPaused}
/>
</div>
);
}
```
### UI Components
Enable built-in UI controls for torch, zoom, and camera switching:
```jsx
import { Scanner } from '@yudiel/react-qr-scanner';
function UIComponentsExample() {
return (
<Scanner
onScan={(result) => console.log(result)}
components={{
audio: true, // Play beep sound on scan
onOff: true, // Show camera on/off button
torch: true, // Show torch/flashlight button (if supported)
zoom: true, // Show zoom control (if supported)
finder: true, // Show finder overlay
}}
// Custom sound (base64 encoded audio)
sound="data:audio/mp3;base64,YOUR_BASE64_AUDIO_HERE"
/>
);
}
```
## API Reference
### Scanner Props
| Prop | Type | Required | Default | Description |
|------------------|-----------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------------------------|
| `onScan` | `(detectedCodes: IDetectedBarcode[]) => void` | Yes | - | Called when one or more barcodes are detected. |
| `onError` | `(error: IScannerError) => void` | No | - | Called with a typed error if the camera fails to start or detection fails. See [Type Definitions](#iscannererror). |
| `constraints` | `MediaTrackConstraints` | No | `{}` | Media track constraints applied to the video stream (e.g., `facingMode`, `deviceId`). |
| `formats` | `BarcodeFormat[]` | No | All | Barcode formats to detect. If omitted, all supported formats are detected. |
| `paused` | `boolean` | No | `false` | If `true`, the scanner pauses and displays the last frame. |
| `children` | `ReactNode` | No | - | Custom children to render inside the scanner container. |
| `components` | `IScannerComponents` | No | `{}` | Built-in UI components and optional tracker. |
| `tracker` | `TrackFunction` | No | - | Shortcut for `components.tracker`. Overrides it if both are set. |
| `styles` | `IScannerStyles` | No | `{}` | Inline CSS for scanner elements. |
| `classNames` | `IScannerClassNames` | No | `{}` | Class names for scanner elements. |
| `scanDelay` | `number` | No | `0` | Minimum delay (ms) between `onScan` calls when `allowMultiple` is `true`. |
| `retryDelay` | `number` | No | `500` / `33` | Minimum delay (ms) between detection attempts. Default is 500 with no tracker, 33 (≈30 fps) with a tracker. |
| `allowMultiple` | `boolean` | No | `false` | If `true`, allows the same barcode to trigger `onScan` repeatedly. |
| `sound` | `boolean \| string` | No | `true` | Plays a beep on successful scan. Pass a URL/data URI for a custom sound. |
| `startTimeoutMs` | `number` | No | `3000` | Maximum time (ms) to wait for `play()` before failing with a timeout error. |
| `settleDelayMs` | `number` | No | `500` | Delay (ms) after `play()` before reading camera capabilities/settings. Set lower for faster devices. |
### Scanner Ref
`Scanner` is a `forwardRef` component. Pass a ref to access the underlying
video element and the active `MediaStream`:
```tsx
import { Scanner, type IScannerHandle } from '@yudiel/react-qr-scanner';
import { useRef } from 'react';
function App() {
const scannerRef = useRef<IScannerHandle>(null);
function snapshot() {
const video = scannerRef.current?.getVideoElement();
if (!video) return;
// ...take a still frame from the video element
}
return <Scanner ref={scannerRef} onScan={console.log} />;
}
```
The ref shape is:
```ts
interface IScannerHandle {
getVideoElement: () => HTMLVideoElement | null;
getStream: () => MediaStream | null;
}
```
### useDevices Hook
Returns an array of available video input devices (cameras).
```typescript
const devices = useDevices();
// Returns: MediaDeviceInfo[]
```
**Example:**
```jsx
import { useDevices } from '@yudiel/react-qr-scanner';
function CameraList() {
const devices = useDevices();
return (
<ul>
{devices.map((device) => (
<li key={device.deviceId}>
{device.label || `Camera ${device.deviceId}`}
</li>
))}
</ul>
);
}
```
### Utilities
#### `isBarcodeDetectorSupported()`
Returns `true` if the browser ships a native `BarcodeDetector`. Useful for
gating UI on native vs. polyfill detection.
```ts
import { isBarcodeDetectorSupported } from '@yudiel/react-qr-scanner';
if (!isBarcodeDetectorSupported()) {
console.info('Using the polyfill detector; performance will be lower.');
}
```
#### `createScannerError(cause)`
Maps a `DOMException`, `Error`, or string to an `IScannerError`. The `Scanner`
component calls this internally before invoking `onError`; export is provided
for callers building their own integrations on top of `useDevices` /
`useCamera`.
#### Advanced: customizing the detector engine
The library re-exports two escape hatches from `barcode-detector` for swapping
out the ZXing engine the polyfill uses (e.g., to host the WASM yourself, or to
swap in a different build):
```ts
import {
prepareZXingModule,
setZXingModuleOverrides,
} from '@yudiel/react-qr-scanner';
// Override the location the polyfill loads its WASM from
setZXingModuleOverrides({
locateFile: (path) => `/static/${path}`,
});
// Or pre-warm the engine before the first scan
await prepareZXingModule();
```
See the [`barcode-detector` docs](https://github.com/Sec-ant/barcode-detector)
for the full API.
## Supported Formats
The library supports detection of the following barcode formats:
| 1D Barcodes | 2D Barcodes |
|------------------|---------------|
| Codabar | Aztec |
| Code 39 | Data Matrix |
| Code 93 | Matrix Codes |
| Code 128 | Maxi Code |
| Databar | Micro QR Code |
| Databar Expanded | PDF 417 |
| Dx Film Edge | QR Code |
| EAN 8 | rMQR Code |
| EAN 13 | |
| ITF | |
| Linear Codes | |
| UPC A | |
| UPC E | |
To detect specific formats only:
```jsx
<Scanner
onScan={(result) => console.log(result)}
formats={['qr_code', 'ean_13', 'code_128']}
/>
```
## Type Definitions
### `BarcodeFormat`
```typescript
type BarcodeFormat =
| 'aztec'
| 'code_128'
| 'code_39'
| 'code_93'
| 'codabar'
| 'databar'
| 'databar_expanded'
| 'data_matrix'
| 'dx_film_edge'
| 'ean_13'
| 'ean_8'
| 'itf'
| 'maxi_code'
| 'micro_qr_code'
| 'pdf417'
| 'qr_code'
| 'rm_qr_code'
| 'upc_a'
| 'upc_e'
| 'linear_codes'
| 'matrix_codes'
| 'unknown';
```
### `IDetectedBarcode`
```typescript
interface IDetectedBarcode {
boundingBox: IBoundingBox;
cornerPoints: IPoint[];
format: string;
rawValue: string;
}
```
### `IBoundingBox`
```typescript
interface IBoundingBox {
x: number;
y: number;
width: number;
height: number;
}
```
### `IPoint`
```typescript
interface IPoint {
x: number;
y: number;
}
```
### `IScannerComponents`
```typescript
interface IScannerComponents {
tracker?: TrackFunction;
onOff?: boolean;
torch?: boolean;
zoom?: boolean;
finder?: boolean;
}
```
### `IScannerError`
```typescript
type ScannerErrorKind =
| 'permission-denied' // user denied camera permission
| 'no-camera' // no video input device found
| 'in-use' // device locked by another app/tab
| 'overconstrained' // requested constraints can't be satisfied
| 'insecure-context' // not HTTPS / localhost
| 'unsupported' // browser lacks getUserMedia / Stream API
| 'aborted' // request was aborted
| 'security' // SecurityError raised
| 'type-error' // bad input passed to getUserMedia
| 'unknown'; // unmatched DOMException or non-Error cause
interface IScannerError {
kind: ScannerErrorKind;
message: string;
cause: unknown; // the original DOMException / Error
}
```
### `IScannerHandle`
```typescript
interface IScannerHandle {
getVideoElement: () => HTMLVideoElement | null;
getStream: () => MediaStream | null;
}
```
### `TrackFunction`
```typescript
type TrackFunction = (
detectedCodes: IDetectedBarcode[],
ctx: CanvasRenderingContext2D
) => void;
```
### `IScannerStyles`
```typescript
interface IScannerStyles {
container?: CSSProperties;
video?: CSSProperties;
finderBorder?: number;
}
```
### `IScannerClassNames`
```typescript
interface IScannerClassNames {
container?: string;
video?: string;
}
```
## Browser Support
This library requires support for:
- **getUserMedia API**: Camera access
- **Barcode Detection API**: Barcode scanning (polyfilled
via [barcode-detector](https://www.npmjs.com/package/barcode-detector))
- **Canvas API**: Drawing tracking overlays
**Supported Browsers:**
- Chrome/Edge 88+
- Firefox 90+ (with polyfill)
- Safari 14+ (with polyfill)
- Mobile browsers (iOS Safari 14.5+, Chrome Mobile)
The library uses `webrtc-adapter` for cross-browser compatibility.
## Troubleshooting
### `onError` fires with `kind: 'permission-denied'`
The user (or a previously remembered choice) denied camera access. Surface a
prompt asking them to re-grant permission in their browser. In Chrome:
site-info chip → Camera → Allow. In Safari: Settings → Websites → Camera.
### `kind: 'no-camera'`
`enumerateDevices()` returned no video inputs. Common causes:
- No camera connected (desktop without a webcam).
- A previously selected `deviceId` is no longer connected. Pass a different
`deviceId`, or omit `constraints.deviceId` entirely to fall back to
`facingMode`.
### `kind: 'in-use'`
Another app or browser tab has the camera locked. On Windows, the desktop
Camera app is a common culprit; on mobile, switching apps mid-scan can do this
too. The library can't recover from this; close the other consumer and
remount the `Scanner`.
### `kind: 'overconstrained'`
The combination of constraints you passed can't be satisfied by any connected
camera. Most often this is a `deviceId` + `facingMode` conflict (the library
already strips `facingMode` when a `deviceId` is present, but a user-passed
`width`/`height`/`aspectRatio` might still be impossible). Drop the failing
constraint and retry.
### `kind: 'insecure-context'`
Camera APIs require a secure origin. Serve over HTTPS, or develop on
`localhost` (Chrome / Firefox / Safari all consider `localhost` secure).
### Scanner runs but never detects anything
- Make sure there's enough light and the camera is in focus.
- Try removing the `formats` prop to detect all formats. The format you
expected might not be in the list.
- If `isBarcodeDetectorSupported()` returns `false`, the polyfill WASM is
doing the work. Check the Network tab for the WASM file (404 → host with
`setZXingModuleOverrides({ locateFile })`).
### iOS Safari plays no sound on the first scan
iOS requires a user gesture before audio can play. The very first scan after
page load may be silent; subsequent scans (after any user interaction) play
normally.
### Torch turns off when I zoom in
This is intentional. Mobile browsers can't mix ImageCapture (torch) and
non-ImageCapture (zoom) constraints simultaneously. The library disables the torch
before applying zoom and updates the React state to match. Re-toggle torch
after the zoom change settles.
### Next.js / SSR errors at build time
Import the scanner lazily so it never runs on the server:
```tsx
import dynamic from 'next/dynamic';
const Scanner = dynamic(
() => import('@yudiel/react-qr-scanner').then((m) => m.Scanner),
{ ssr: false },
);
```
`useDevices()` is also browser-only. Only call it inside `'use client'`
components (App Router) or with `dynamic({ ssr: false })` wrappers.
## Limitations
- **HTTPS or localhost required**: Due to browser security restrictions, camera access only works on secure contexts
(HTTPS or localhost).
- **iOS audio limitations**: Beep sound on iOS Safari requires user interaction before playing. The first scan after
the page load may not play sound.
- **Server-Side Rendering (SSR)**: This library requires browser APIs and will not work during SSR. Ensure you only
import and use it in client-side code:
```jsx
// Next.js example
import dynamic from 'next/dynamic';
const Scanner = dynamic(
() => import('@yudiel/react-qr-scanner').then((mod) => mod.Scanner),
{ ssr: false }
);
```
- **Mobile browser constraints**: Some mobile browsers cannot use torch and zoom simultaneously. The library
automatically disables the torch when the zoom is activated to prevent conflicts.
## Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md) for local-dev setup, code style, PR
process, and the project layout. By participating you agree to abide by the
[Code of Conduct](./CODE_OF_CONDUCT.md). Report security issues via GitHub's
private vulnerability reporting flow.
## License
[MIT](https://github.com/yudielcurbelo/react-qr-scanner/blob/main/LICENSE)
© [Yudiel Curbelo](https://github.com/yudielcurbelo)