Functions

useBarcodeDetector

Reactive wrapper around the Barcode Detection API.

useBarcodeDetector

Reactive wrapper around the Barcode Detection API.

Accepts any source BarcodeDetector#detect understands: HTMLImageElement, SVGImageElement, HTMLVideoElement, HTMLCanvasElement, ImageBitmap, OffscreenCanvas, VideoFrame, Blob, ImageData.

For an HTMLVideoElement source, manages a getUserMedia stream and runs detection on each animation frame. For any other source, runs detection whenever the source ref changes (with immediate: true) or on demand via detect().

The composable defaults to immediate: false. Browsers (notably Safari/iOS) require a user gesture for getUserMedia and video.play(), so call start() from a click handler. For non-video sources you can opt back into auto-detect with immediate: true.

Usage

Demo

Point a camera at any QR code or supported barcode. Detected polygons are drawn over the live feed and listed below.

<script setup lang="ts">
import { useTemplateRef } from 'vue'
import { BarcodeDetectorOverlay, useBarcodeDetector } from '@orbisk/vue-use-barcode-detection'

const video = useTemplateRef<HTMLVideoElement>('video')
const { isSupported, supportedFormats, detected, error, isActive, start, stop } =
  useBarcodeDetector(video)
</script>

<template>
  <p v-if="!isSupported"><code>BarcodeDetector</code> is not available.</p>
  <p v-if="error">{{ error.message }}</p>

  <div class="stage">
    <video ref="video" playsinline muted />
    <BarcodeDetectorOverlay :detected="detected" :source="video" />
  </div>

  <button @click="isActive ? stop() : start()">
    {{ isActive ? 'Stop' : 'Start camera' }}
  </button>

  <ul>
    <li v-for="(b, i) in detected" :key="i">
      <strong>{{ b.format }}</strong><code>{{ b.rawValue }}</code>
    </li>
  </ul>
</template>

Prefer the <UseBarcodeDetector /> component below if you want the same thing in one tag, with the overlay built in.

Live camera

start() must be called from a user gesture (e.g. a button click) so Safari/iOS will accept the getUserMedia + video.play() calls.

<script setup lang="ts">
import { useTemplateRef } from 'vue'
import { useBarcodeDetector } from '@orbisk/vue-use-barcode-detection'

const video = useTemplateRef<HTMLVideoElement>('video')
const { isSupported, detected, error, isActive, start, stop } = useBarcodeDetector(video)
</script>

<template>
  <video ref="video" playsinline muted />
  <button @click="isActive ? stop() : start()">
    {{ isActive ? 'Stop' : 'Start camera' }}
  </button>
</template>

Static image

Image / blob / canvas sources have no autoplay concerns, so opt back into auto-detect with immediate: true to scan as soon as the source is ready.

<script setup lang="ts">
import { useTemplateRef } from 'vue'
import { useBarcodeDetector } from '@orbisk/vue-use-barcode-detection'

const img = useTemplateRef<HTMLImageElement>('img')
const { detected } = useBarcodeDetector(img, { immediate: true })
</script>

<template>
  <img ref="img" src="/barcode.png" alt="" />
</template>

Manual detection on a Blob

const { detect } = useBarcodeDetector()
const file = await fetch('/barcode.png').then((r) => r.blob())
const result = await detect(file)

Scan once

Stop the camera as soon as a barcode is found. Combine with accept to keep scanning until a matching barcode shows up. Call start() from a user gesture to begin (and again to re-arm after a stop).

// Stop on any barcode
const { detected, start } = useBarcodeDetector(video, { once: true })
// later, from a click handler:
await start()

// Stop only on QR codes
useBarcodeDetector(video, {
  once: true,
  accept: (b) => b.format === 'qr_code',
})

// Stop only on values starting with `XX-`
useBarcodeDetector(video, {
  once: true,
  accept: (b) => b.rawValue.startsWith('XX-'),
})

accept filters detected itself — non-matching barcodes never appear there, never trigger watchers, and never trigger once. detected keeps the result that triggered the stop. Call start() to scan again.

Reactive options

formats and once accept a ref or a getter, so you can flip them at runtime without re-mounting the composable. Useful for "scan only QR codes" toggles, multi-mode scanners, etc.

const onlyQr = ref(false)

const formats = computed<BarcodeFormat[] | undefined>(() =>
  onlyQr.value ? ['qr_code'] : undefined,
)

useBarcodeDetector(video, {
  formats, // rebuilds the BarcodeDetector when `onlyQr` flips
  once: onlyQr, // stop on first hit *only* in QR-only mode
})

accept is intentionally not MaybeRefOrGettertoValue can't tell a predicate from a getter. Capture refs in your closure instead:

const prefix = ref('')
useBarcodeDetector(video, {
  accept: (b) => !prefix.value || b.rawValue.startsWith(prefix.value),
})

Options

NameTypeDefaultDescription
formatsMaybeRefOrGetter<BarcodeFormat[] | undefined>all formats supported by browserRestrict detection to specific formats. Pass a ref or getter to switch formats at runtime — the underlying BarcodeDetector is rebuilt on change.
immediatebooleanfalseAuto-start (camera for video sources, run detect() on change otherwise) once the source is available. Defaults to false because getUserMedia / video.play() need a user gesture in Safari/iOS.
cameraboolean | MediaTrackConstraintstrueFor video sources: true calls getUserMedia with rear camera; pass constraints to override; false skips camera setup so you can supply your own stream.
onceMaybeRefOrGetter<boolean>falseStop after the first accepted detection. Pair with accept to stop only on matching barcodes. Reactive: read on each detection, so flipping the ref live is honored. Call start() to re-arm.
accept(b: DetectedBarcode) => booleanundefinedPredicate gating which detections count. Non-matching barcodes are filtered out of detected and don't trigger once. Plain function — closures capture reactive deps for runtime behavior.
windowWindowdefaultWindowCustom window reference (SSR / iframe).

Returns

NameTypeDescription
isSupportedComputedRef<boolean>Whether BarcodeDetector exists on window.
supportedFormatsShallowRef<BarcodeFormat[]>Formats reported by the browser once the detector is created.
detectedShallowRef<DetectedBarcode[]>Latest detection result (filtered through accept if set).
rejectedShallowRef<DetectedBarcode[]>Detections the accept predicate filtered out. Always empty otherwise.
errorShallowRef<Error | null>Set when permission is denied or the API is unavailable.
isActiveShallowRef<boolean>Whether the camera + detection loop is running (video sources only).
detect(source?: BarcodeImageSource | null) => Promise<DetectedBarcode[]>Run a single detection. Falls back to the configured source ref.
start() => Promise<void>Start the camera stream and detection loop. No-op for non-video sources.
stop() => voidStop the loop and release the media stream.

Component

<UseBarcodeDetector /> is an all-in-one wrapper around the composable. It renders a <video> element plus a default SVG overlay that draws polygons over detected barcodes — drop it in and you have a working scanner.

  • The overlay slot replaces only the default overlay.
  • The default slot is rendered as a sibling after the stage, with full composable state and actions exposed as slot props. Use it for results lists, error messages, controls, etc.
  • The headless prop skips the built-in stage entirely; the default slot becomes the sole rendering and must wire up its own source element via setSource.

Drop-in scanner

Use the default slot to wire start / stop to a button — autoplay is gated behind a user gesture so the component works on Safari/iOS.

<script setup lang="ts">
import { UseBarcodeDetector } from '@orbisk/vue-use-barcode-detection'
</script>

<template>
  <UseBarcodeDetector v-slot="{ start, stop, isActive, detected }">
    <button @click="isActive ? stop() : start()">
      {{ isActive ? 'Stop' : 'Start camera' }}
    </button>
    <ul>
      <li v-for="b in detected" :key="b.rawValue">
        <strong>{{ b.format }}</strong><code>{{ b.rawValue }}</code>
      </li>
    </ul>
  </UseBarcodeDetector>
</template>

Custom UI alongside the scanner

<template>
  <UseBarcodeDetector v-slot="{ detected, error, isSupported }">
    <p v-if="!isSupported">BarcodeDetector is not available.</p>
    <p v-else-if="error">{{ error.message }}</p>
    <ul>
      <li v-for="b in detected" :key="b.rawValue">
        <strong>{{ b.format }}</strong><code>{{ b.rawValue }}</code>
      </li>
    </ul>
  </UseBarcodeDetector>
</template>

Custom overlay only

The overlay slot receives the live source element — pass it to <BarcodeDetectorOverlay :source="source"> (or your own SVG) and the overlay tracks the visible source rect automatically.

<template>
  <UseBarcodeDetector>
    <template #overlay="{ detected, source }">
      <BarcodeDetectorOverlay
        :detected="detected"
        :source="source"
        stroke="hotpink"
        :stroke-width="6"
      />
    </template>
  </UseBarcodeDetector>
</template>

Fully headless

headless skips the built-in <video> stage so the default slot does all the rendering. Bind any source BarcodeDetector#detect understands — <video>, <img>, <canvas> — through setSource. The demo below feeds a QR generated with useQRCode (from @vueuse/integrations) into an <img> and lets the wrapper drive detection — no camera required.

<script setup lang="ts">
import { ref, useTemplateRef, watch } from 'vue'
import {
  type DetectedBarcode,
  type UseBarcodeDetectorReturn,
} from '@orbisk/vue-use-barcode-detection'
import { useQRCode } from '@vueuse/integrations/useQRCode'

const text = ref('Hello, headless!')
const prefix = ref('')

const accept = (b: DetectedBarcode) => !prefix.value || b.rawValue.startsWith(prefix.value)
const qrSrc = useQRCode(text, { width: 320, margin: 4 })

// The wrapper exposes the composable's return — re-run detect() when only
// the predicate changes (the <img> ref is unchanged in that case, so the
// composable's source watcher wouldn't fire on its own).
const scanner = useTemplateRef<UseBarcodeDetectorReturn>('scanner')
watch(prefix, () => void scanner.value?.detect())
</script>

<template>
  <UseBarcodeDetector
    ref="scanner"
    headless
    immediate
    :accept="accept"
    v-slot="{ setSource, source, detect, detected, rejected }"
  >
    <input v-model="text" type="text" />
    <input v-model="prefix" type="text" placeholder="required prefix (any)" />

    <div class="stage">
      <img :ref="setSource" :src="qrSrc" alt="QR" @load="detect()" />
      <BarcodeDetectorOverlay
        :detected="detected"
        :rejected="rejected"
        :source="source"
        :label="(b, accepted) => (accepted ? b.rawValue : 'invalid')"
      />
    </div>
  </UseBarcodeDetector>
</template>

For a live-camera headless setup, bind a <video> instead and wire start / stop / isActive from the slot to a button — the same wrapper without immediate.

<template>
  <UseBarcodeDetector headless v-slot="{ setSource, detected, start, stop, isActive }">
    <video :ref="setSource" playsinline muted autoplay />
    <button @click="isActive ? stop() : start()">
      {{ isActive ? 'Stop' : 'Start' }}
    </button>
    <pre>{{ detected }}</pre>
  </UseBarcodeDetector>
</template>

Component props

NameTypeDefaultDescription
formatsBarcodeFormat[]allRestrict detection to specific formats.
immediatebooleanfalseAuto-start the camera + detection loop once the video element is mounted. Defaults to false because getUserMedia / video.play() need a user gesture in Safari/iOS.
cameraboolean | MediaTrackConstraintstruetrue calls getUserMedia with rear camera; pass constraints to override; false skips.
oncebooleanfalseStop after the first accepted detection. Combine with accept to stop only on matching barcodes.
accept(b: DetectedBarcode) => booleanundefinedPredicate gating which detections count. Non-matching barcodes are filtered out of detected.
headlessbooleanfalseSkip the built-in stage; the default slot is the only rendering and must bind its own video.

Default slot props

Slot propType
isSupportedboolean
supportedFormatsBarcodeFormat[]
detectedDetectedBarcode[]
rejectedDetectedBarcode[] — barcodes the accept predicate filtered out
errorError | null
isActiveboolean
detect(source?: BarcodeImageSource | null) => Promise<DetectedBarcode[]>
start() => Promise<void>
stop() => void
sourceHTMLElement | null
setSource(el: Element | null) => void — bind via :ref="setSource"

overlay slot props

Slot propTypeDescription
detectedDetectedBarcode[]Latest detection result (filtered through accept if set).
rejectedDetectedBarcode[]Barcodes the accept predicate filtered out.
sourceHTMLElement | nullThe internal source element (the <video> from the built-in stage). Pass to <BarcodeDetectorOverlay :source="source"> to keep the auto-tracked alignment.

BarcodeDetectorOverlay

The default overlay is also exported as a standalone component. Drop it into a custom overlay slot (or anywhere over a <video> / <img>) when you want to keep the default look but add elements alongside it.

The demo above generates a QR with useQRCode from @vueuse/integrations (you'll need to install @vueuse/integrations and qrcode for this), runs useBarcodeDetector on the resulting <img>, and overlays the detected polygons. Type a required prefix to flip the polygon between accepted (green) and rejected (red) colors live.

<script setup lang="ts">
import { ref, useTemplateRef, watch } from 'vue'
import {
  BarcodeDetectorOverlay,
  type DetectedBarcode,
  useBarcodeDetector,
} from '@orbisk/vue-use-barcode-detection'
import { useQRCode } from '@vueuse/integrations/useQRCode'

const text = ref('Hello, scanner!')
const prefix = ref('')

const accept = (b: DetectedBarcode) =>
  !prefix.value || b.rawValue.startsWith(prefix.value)

// Fixed-size QR keeps the viewBox reactive-safe: img.naturalWidth is a
// non-reactive DOM property, so a computed reading it would never refresh
// once the image loads, leaving the SVG with a 0×0 coordinate space.
// `margin: 4` keeps the QR-spec quiet zone — Chrome's BarcodeDetector
// frequently refuses to lock on QRs with a smaller margin.
const qrSrc = useQRCode(text, { width: 320, margin: 4 })

const img = useTemplateRef<HTMLImageElement>('img')
const { detect, detected, rejected } = useBarcodeDetector(img, { accept })

watch([qrSrc, prefix], () => {
  if (img.value?.complete) void detect()
})
</script>

<template>
  <input v-model="text" />
  <input v-model="prefix" placeholder="required prefix" />
  <div class="stage">
    <!-- No `crossorigin` — data: URLs are same-origin; setting it can taint
         the canvas BarcodeDetector reads from. -->
    <img ref="img" :src="qrSrc" @load="detect" />
    <BarcodeDetectorOverlay
      :detected="detected"
      :rejected="rejected"
      :source="img"
      :label="(b, accepted) => (accepted ? b.rawValue : 'invalid')"
    />
  </div>
</template>
<script setup lang="ts">
import { BarcodeDetectorOverlay, UseBarcodeDetector } from '@orbisk/vue-use-barcode-detection'
</script>

<template>
  <UseBarcodeDetector>
    <template #overlay="{ detected, source }">
      <BarcodeDetectorOverlay :detected="detected" :source="source" stroke="hotpink" />
      <span class="badge">{{ detected.length }} found</span>
    </template>
  </UseBarcodeDetector>
</template>
PropTypeDefaultDescription
detectedDetectedBarcode[]requiredAccepted barcodes — drawn with the primary fill/stroke.
rejectedDetectedBarcode[][]Rejected barcodes — drawn with the secondary fill/stroke.
sourceHTMLElement | nullnullThe <video> / <img> / <canvas> the overlay is drawn over. The overlay tracks its visible rect (mirroring object-fit + object-position) and aligns the SVG to it.
viewBoxstringautoSVG viewBox. Auto-derived from source's intrinsic size; pass to override (e.g. for off-screen detection sources).
fillstring'rgba(0, 200, 120, 0.15)'Fill for accepted polygons.
strokestring'rgb(0, 200, 120)'Stroke for accepted polygons.
rejectedFillstring'rgba(220, 60, 60, 0.12)'Fill for rejected polygons.
rejectedStrokestring'rgb(220, 60, 60)'Stroke for rejected polygons.
strokeWidthnumber4Stroke width (in viewBox units, but rendered with non-scaling-stroke).
label(b, accepted) => string | null | undefinedundefinedRender a label inside each polygon. Receives the barcode and whether it was accepted; return falsy to skip that detection's label. Long strings are auto-truncated with to fit the polygon width; very narrow polygons get no label.
labelColorstring'#fff'Label text fill color.
labelFontSizenumber24Label font size in viewBox units. The polygon stroke is reused as the text outline so labels stay legible on busy backgrounds. Lower this if your barcodes are small relative to the source — auto-truncate is keyed off this size.

Highlight accepted vs. rejected barcodes

Combine the accept predicate with the dual-color overlay to give users instant feedback on why a scan was ignored — e.g. "yes I see this code, but it's not a QR code so I'm waiting for one." <UseBarcodeDetector> wires this up automatically: the default overlay receives both detected (accepted) and rejected lists, and renders them in green / red respectively.

<script setup lang="ts">
import type { DetectedBarcode } from '@orbisk/vue-use-barcode-detection'
import { UseBarcodeDetector } from '@orbisk/vue-use-barcode-detection'

// Only QR codes count — everything else gets the rejected styling.
const onlyQr = (b: DetectedBarcode) => b.format === 'qr_code'
</script>

<template>
  <UseBarcodeDetector
    once
    :accept="onlyQr"
    v-slot="{ detected, rejected, isActive, start, stop }"
  >
    <button @click="isActive ? stop() : start()">
      {{ isActive ? 'Stop' : 'Start camera' }}
    </button>
    <p v-if="rejected.length && !detected.length">
      Found {{ rejected.length }} barcode(s), but none are QR codes — keep moving the camera.
    </p>
    <ul>
      <li v-for="b in detected" :key="b.rawValue">
        <strong>{{ b.format }}</strong><code>{{ b.rawValue }}</code>
      </li>
    </ul>
  </UseBarcodeDetector>
</template>

If you're using the composable directly, pull rejected from the return and pass it to <BarcodeDetectorOverlay> yourself:

<script setup lang="ts">
import { useTemplateRef } from 'vue'
import {
  BarcodeDetectorOverlay,
  type DetectedBarcode,
  useBarcodeDetector,
} from '@orbisk/vue-use-barcode-detection'

const video = useTemplateRef<HTMLVideoElement>('video')
const { detected, rejected, start, isActive } = useBarcodeDetector(video, {
  accept: (b: DetectedBarcode) => b.format === 'qr_code',
  once: true,
})
</script>

<template>
  <div class="stage">
    <video ref="video" playsinline muted />
    <BarcodeDetectorOverlay :detected="detected" :rejected="rejected" :source="video" />
  </div>
  <button v-if="!isActive" @click="start">Start camera</button>
</template>

Want different colors? Pass fill / stroke / rejectedFill / rejectedStroke directly:

<BarcodeDetectorOverlay
  :detected="detected"
  :rejected="rejected"
  :source="video"
  stroke="lime"
  rejected-stroke="orange"
  rejected-fill="rgba(255, 165, 0, 0.1)"
/>

SSR / hydration

Both the composable and the component are SSR-safe — they don't touch window, navigator, or any browser-only API during setup. All side effects (creating the detector, requesting the camera, starting the RAF loop) are deferred to onMounted, so server-side rendering produces only the static <video> element with no overlay.

To prevent hydration mismatches in slot-rendered UI, the isSupported slot prop is gated on useMounted: it stays false during SSR and during the client's first render, then flips to its real value once the component mounts. This means a template like <p v-if="!isSupported">unsupported</p> won't trigger a hydration warning.

If you want to skip server rendering entirely (e.g. in Nuxt), wrap the component in <ClientOnly>:

<template>
  <ClientOnly>
    <UseBarcodeDetector />
  </ClientOnly>
</template>

Exposed instance

The component exposes the entire return of useBarcodeDetector on its instance — so you can grab a template ref and call start(), stop(), etc. from outside.

<script setup lang="ts">
import { useTemplateRef } from 'vue'
import { UseBarcodeDetector } from '@orbisk/vue-use-barcode-detection'

const scanner = useTemplateRef<InstanceType<typeof UseBarcodeDetector>>('scanner')

function rescan() {
  scanner.value?.stop()
  scanner.value?.start()
}
</script>

<template>
  <UseBarcodeDetector ref="scanner" />
  <button @click="rescan">Restart</button>
</template>
Copyright © 2026