Tutorial8 min read

Building a barcode and QR scanner in Capacitor

Add fast, native barcode and QR code scanning to your Capacitor app, handle camera permissions cleanly, and ship scanning-flow changes over the air without a store release.

Barcode and QR scanning shows up everywhere — loyalty cards, ticketing, inventory, onboarding via QR. In a Capacitor app you want a native scanner (fast, uses the real camera pipeline), not a slow JavaScript decoder on a video stream. This guide covers adding native scanning, handling camera permissions cleanly, and iterating the scan flow over the air with OtaKit.

1. Install a native scanner plugin

Use a maintained Capacitor barcode plugin backed by ML Kit (Android) and the native scanner (iOS). It decodes on-device, fast, across the common symbologies:

npx cap sync

2. Declare camera permissions

  • iOS: add NSCameraUsageDescription with a clear, honest reason string.
  • Android: the camera permission in the manifest.

These are native and belong in the store build. The permission copy, though, matters for approval and trust — say what you scan and why.

3. Request permission, then scan

async function scan() {
  const granted = await BarcodeScanner.requestPermissions();
  if (granted.camera !== 'granted') return showPermissionRationale();

  const { barcodes } = await BarcodeScanner.scan();
  if (barcodes.length) handleResult(barcodes[0].rawValue);
}

4. Handle the result in your web layer

What happens after a successful scan — look up the code, validate a ticket, add to a cart, route to a screen — is all web-layer logic. That's exactly what you'll refine as requirements change, and it ships over the air:

otakit upload --release production

Handle the “permission denied” path deliberately. Show a rationale and a route to Settings rather than a dead scan button — and since that's UI, you can improve it over the air after you see how users actually hit it.

Where to go next

See Setup to add OtaKit alongside the scanner, and offline support if scanning needs to work without a connection.

Related docs