UNPKG

@yudiel/react-qr-scanner

Version:
673 lines (529 loc) 20.7 kB
# @yudiel/react-qr-scanner [![npm version](https://img.shields.io/npm/v/@yudiel/react-qr-scanner.svg)](https://www.npmjs.com/package/@yudiel/react-qr-scanner) [![npm downloads](https://img.shields.io/npm/dm/@yudiel/react-qr-scanner.svg)](https://www.npmjs.com/package/@yudiel/react-qr-scanner) [![license](https://img.shields.io/npm/l/@yudiel/react-qr-scanner.svg)](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)