The Challenge: QR Codes Without Libraries
Most developers reach for npm install qrcode and move on. But one developer decided to implement the full QR Code Model 2 algorithm in vanilla JavaScript—zero external dependencies, no server, just a single HTML file. The result is a free, client-side QR code generator that runs entirely in the browser.
Technical Deep Dive: How QR Codes Actually Work
The ISO spec for QR codes is 126 pages. The implementation covers six major steps:
1. Data Encoding
Three modes based on content:
- Numeric (0-9): packs 3 digits into 10 bits
- Alphanumeric (0-9 A-Z $%*+-./:space): 2 chars into 11 bits
- Byte (everything else): UTF-8, one byte per 8 bits
The encoder auto-detects the mode and finds the minimum QR version (1–40) that fits:
function detectMode(text) {
if (/^\d+$/.test(text)) return NUMERIC_MODE;
if (text.split('').every(c => ALPHANUMS.includes(c))) return ALPHANUM_MODE;
return BYTE_MODE;
}
2. Reed-Solomon Error Correction
QR codes survive up to 30% damage (at ECL H) using Reed-Solomon codes—the same algorithm used in CDs and deep-space transmissions. The implementation uses Galois Field GF(256) with precomputed EXP and LOG tables for fast multiplication:
const GF = (() => {
const EXP = new Uint8Array(512);
const LOG = new Uint8Array(256);
let x = 1;
for (let i = 0; i < 255; i++) {
EXP[i] = x;
LOG[x] = i;
x <<= 1;
if (x & 0x100) x ^= 0x11d; // reduce mod primitive polynomial
}
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
return {
mul(a, b) { return (a === 0 || b === 0) ? 0 : EXP[LOG[a] + LOG[b]]; },
// ...
};
})();
3. Data Interleaving
For larger versions, data splits into multiple blocks, each with its own RS codewords. Blocks are interleaved byte-by-byte so physical damage hits different blocks and can be recovered.
4. Matrix Construction
A QR code matrix (21×21 for version 1, up to 177×177 for version 40) includes:
- Finder patterns (three 7×7 squares in corners)
- Alignment patterns (smaller squares for versions 2+)
- Timing patterns (alternating dark/light rows/columns)
- Format information (ECL and mask pattern, BCH(15,5) encoded)
- Version information (for versions 7+, BCH(18,6) encoded)
- Data modules (placed in a specific zigzag path)
5. Masking
Eight mask functions are evaluated (e.g., (row + col) % 2 === 0). The encoder tries all 8, scores each using a 4-rule penalty system, and picks the lowest-penalty mask:
const MASK_FNS = [
(r,c) => (r+c)%2===0,
(r,c) => r%2===0,
(r,c) => c%3===0,
(r,c) => (r+c)%3===0,
(r,c) => (Math.floor(r/2)+Math.floor(c/3))%2===0,
(r,c) => (r*c)%2+(r*c)%3===0,
(r,c) => ((r*c)%2+(r*c)%3)%2===0,
(r,c) => ((r+c)%2+(r*c)%3)%2===0
];
6. Format Info Placement
The chosen ECL and mask index are BCH-encoded into a 15-bit format string. The developer precomputed all 32 possible values to keep runtime code simple.
The Hardest Bug
Format info placement had a subtle off-by-one. The spec places bits 0–5 of the 15-bit format word in rows 0–5 of column 8, bit 7 in row 7, and bit 8 in row 8. The bit ordering was reversed for horizontal vs vertical copies, producing valid-looking but wrong format areas. The fix: explicitly write each position separately rather than looping symmetrically.
Testing: 202 Cases, No Test Framework
The test suite covers every layer with 202 tests—no Jest, no Mocha, just a 30-line inline test runner. Key test categories:
- GF(256) arithmetic: 14 tests
- RS generator: 8 tests
- Mode detection: 12 tests
- UTF-8 encoding: 10 tests
- Data capacity table: 10 tests (versions 1–40, all 4 ECL levels)
- charCountBits: 9 tests (bit width transitions at v9/v26)
- encodeData version selection: 12 tests
- BitStream: 8 tests
- generateQR shape: 12 tests
- Module values: 10 tests
- Version sizes: 8 tests (size = version × 4 + 17)
- Mask patterns: 8 tests
- Version info BCH: 6 tests (known spec values v7–v10, v40)
- Format strings: 6 tests (all ECL levels)
- Alignment positions: 6 tests (v1, v2, v7, v40)
- Codeword interleaving: 6 tests
- ECL level comparisons: 8 tests
- Known RS vector: 6 tests (exact byte match from QR spec)
- Finder pattern modules: 8 tests
- Edge cases: 6 tests (emoji, long URLs, whitespace)
- ECL stress: 6 tests
- Null input handling: 4 tests
The Tool
The generator offers:
- Real-time generation (debounced at 80ms)
- Size selector: 128×128, 256×256, or 512×512 pixels
- Error correction levels: L (7%), M (15%), Q (25%), H (30%)
- Color picker for foreground and background
- PNG download via canvas
- SVG download with crisp vector output
Try it: https://qr-code-generator-e83.pages.dev
The full source is a single index.html—open DevTools to read through. No build step, no framework, no minification. Part of devnestio—a growing collection of zero-dependency developer tools.
Why This Matters
Building a QR code generator from scratch teaches you about error-correcting codes, Galois field arithmetic, and matrix encoding—concepts that apply to many areas of software engineering. Plus, having a zero-dependency tool means it works offline, loads instantly, and is auditable in minutes.


