Skip to content
Devix Open Source

Guide

Getting started

npm install @devix-labs/qr-code
<div id="code"></div>
import { createQrCode } from '@devix-labs/qr-code';
import '@devix-labs/qr-code/styles.css';

const qr = createQrCode(document.querySelector('#code'), {
  value: 'https://devix.pk',
  logo: { src: '/logo.svg' },
});

That renders an inline SVG — sharp at any size, printable, and two elements rather than one per module.

A logo in the middle

The feature the most used generator has had asked for three separate times and never shipped. Here it is one option, and it is checked:

createQrCode(el, {
  value: 'https://devix.pk',
  logo: { src: '/logo.svg', size: 0.2 },   // a fifth of the width
});

A logo destroys the modules underneath it, and error correction is a budget. This spends it carefully:

  • the modules under the logo are removed, not covered, so nothing is misread;
  • if the level cannot carry the logo, it is raised to H automatically;
  • if even H cannot, you get a warning naming the largest size that would work — rather than a code that fails on a customer's phone.
import { encode, fitLogo } from '@devix-labs/qr-code';

const code = encode('https://devix.pk', { level: 'M' });
fitLogo(code, { src: '/logo.svg', size: 0.25 });
// { covered: 121, fraction: 0.14, tolerance: 0.15, safe: false, maxSize: 0.13 }

The published tolerances — 7, 15, 25 and 30 per cent — describe damage scattered across the code. A logo is a solid square in the middle, so it wipes out whole codewords in a few blocks instead of a little of each. We measured the real budget by covering codes at every level until they stopped decoding: it is about a third of the nominal figure, and that is what fitLogo uses.

Getting a file out

await qr.toBlob();            // a PNG, at least 512px
await qr.toBlob(1024);        // bigger
qr.toSvg();                   // the SVG as a string
await qr.download('ticket.png');

toBlob rounds the size up to whole module pixels. A fractional module width blurs every edge, and a blurred code is one a phone gives up on.

Scanning

import { createScanner, canScan } from '@devix-labs/qr-code';

if (canScan()) {
  const scanner = createScanner(document.querySelector('video'), {
    onFound: ([code]) => console.log(code.value),
  });
  await scanner.start();
}

The decoding is the browser's own Barcode Detection API. The alternatives bundle a decoder — 57 kB for jsQR, 108 kB for html5-qrcode, both measured — and both were last published years ago with open issues about iOS freezing and webviews refusing to start. What is genuinely hard about scanning is everything around the decode, and that is what this is: permissions, choosing the back camera, the torch, keeping the video sized, and stopping the camera when the tab is hidden.

canScan() tells you whether this browser can, so you can offer an upload instead where it cannot. See Scanning for the support matrix.

Where to go next

Updated 15 Sep 2026