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.