Building a QR Code Generator

Developer Journal9 min read

Building a QR Code Generator in React: What I Learned

I needed a QR code generator for this site. Instead of embedding a third-party widget, I decided to build one from scratch. Here's the real story — the library decisions, the WiFi format rabbit hole, the color contrast mistake that broke scanning, and why I ended up shipping both SVG and PNG export.

H
Hanul Lee

Web developer with 4+ years of experience building production apps with React, Next.js, and TypeScript. Writing from hands-on experience, not theory.

Published April 6, 2026

Key Takeaways

  1. 1Client-side QR generation keeps user data private — nothing leaves the browser
  2. 2The node-qrcode library handles Canvas, Data URL, and SVG output from a single API
  3. 3WiFi QR codes follow a strict format: WIFI:T:WPA;S:ssid;P:password;;
  4. 4QR code scanners need at least 4:1 contrast ratio between foreground and background
  5. 5SVG export for print, PNG export for sharing — offer both

Every developer tool site needs a QR code generator. When I started building Global Free Tools, it was one of the first features on the list. I could have used a pre-built React component and called it done in an afternoon, but I wanted full control over the output format, color customization, and — most importantly — I wanted everything to stay client-side so users' data never leaves their browser. What followed was a week of learning more about QR codes than I ever expected. The ISO/IEC 18004 standard that defines QR codes is surprisingly deep, and even a 'simple' generator touches on error correction algorithms, character encoding edge cases, and the physics of how phone cameras parse contrast patterns. This article covers the real decisions I made and the mistakes I learned from.

Choosing the Right Approach

My first decision was which library to use. The two main contenders in the React ecosystem are qrcode.react and node-qrcode (the 'qrcode' npm package). qrcode.react is a React component that renders QR codes as either SVG or Canvas elements. It's simple to use — you drop in <QRCodeSVG value="hello" /> and you're done. But I quickly hit limitations: I needed programmatic access to the generated image for download functionality, and I wanted to support multiple output formats (Canvas for preview, Data URL for PNG download, SVG string for SVG download) from the same generation call. node-qrcode (imported as 'qrcode') solved all of these problems. Despite the 'node' in its name, it works perfectly in the browser. It exposes three key methods: QRCode.toCanvas() for rendering directly to a canvas element, QRCode.toDataURL() for generating a base64-encoded PNG, and QRCode.toString() with type 'svg' for generating an SVG string. This single library gave me everything I needed. I chose error correction level 'M' (Medium, ~15% recovery) as the default. Level 'L' (7%) is too fragile for printed codes that might get scratched or partially covered. Level 'H' (30%) is overkill for most use cases and makes the QR code denser with more modules, which can cause scanning issues at small sizes. 'M' is the sweet spot.

Why Client-Side?

Privacy was a core requirement. The QR code is generated entirely in the browser using Canvas API. No data is sent to any server — not the URLs, not the WiFi passwords, nothing. This is especially important for the WiFi mode where users enter their actual network credentials.

typescriptCore generation with node-qrcode
import QRCode from "qrcode";

// Canvas rendering for live preview
await QRCode.toCanvas(canvasRef.current, content, {
  width: size,
  margin: 2,
  color: { dark: fgColor, light: bgColor },
  errorCorrectionLevel: "M",
});

// Data URL for PNG download
const dataUrl = await QRCode.toDataURL(content, {
  width: size,
  margin: 2,
  color: { dark: fgColor, light: bgColor },
  errorCorrectionLevel: "M",
});

The WiFi QR Code Challenge

Adding URL and plain text modes was straightforward — you just pass the string to the QR generator. WiFi mode was where things got interesting. The WiFi QR code format is a de facto standard defined by the ZXing barcode library project: WIFI:T:WPA;S:your-ssid;P:your-password;;. The double semicolons at the end are required. The T field specifies the encryption type (WPA, WEP, or nopass for open networks). My first implementation was naive — I just concatenated the strings directly. It worked for simple SSIDs like 'HomeWiFi', but broke immediately when I tested with an SSID containing a semicolon or colon. These characters are delimiters in the WIFI: format, so they need to be escaped with a backslash. I also discovered that some Android phones handle the 'nopass' encryption type differently — older Samsung devices would show a connection error instead of connecting to the open network. Testing was the hardest part. I had to physically scan QR codes with multiple phones: my iPhone 15, an old Android 10 device, and my partner's Pixel. Each phone's camera app parses QR codes slightly differently, and the only way to know if your WiFi QR code actually works is to scan it and watch the phone connect. There's no shortcut for this kind of testing. I also added an email mode (mailto:address?subject=encoded-subject) which was simpler but taught me to always use encodeURIComponent() on the subject line. Spaces, ampersands, and question marks in email subjects will break the mailto URI if not properly encoded.

typescriptWiFi QR content string builder
// The WiFi QR format recognized by iOS and Android
case "wifi":
  return `WIFI:T:${wifiEncryption};S:${wifiSSID};P:${wifiPassword};;`;

// Encryption options: "WPA" | "WEP" | "nopass"
// Double semicolons at the end are required by the spec

Watch Out

If your WiFi SSID or password contains semicolons, colons, backslashes, or commas, they must be escaped with a backslash in the WIFI: format string. My initial implementation skipped this, and the generated QR codes silently failed — the phone would scan them but not connect. Always test WiFi QR codes on real devices.

Color Customization Pitfalls

Users love customizing colors, so I added foreground and background color pickers. This was the feature that taught me the most about how QR codes actually work at the hardware level. My first version let users pick any two colors freely. Within an hour of testing, I discovered the problem: a dark blue foreground (#1a1a8f) on a medium blue background (#6b7db3) looked nice but was completely unscannable. Phone cameras rely on contrast between the dark modules and light background to decode QR data. The ISO standard recommends a minimum contrast ratio of 4:1, but in practice, I found that ratios below 3:1 fail on most devices, and ratios between 3:1 and 4:1 are unreliable in poor lighting. My solution was to add color presets — five pre-tested combinations that all guarantee sufficient contrast: Classic (black on white), Blue (#1e40af on #dbeafe), Green (#166534 on #dcfce7), Purple (#9333ea on #f3e8ff), and Red (#dc2626 on #fef2f2). Each preset uses a dark, saturated foreground on a very light tint of the same hue. Users can still pick custom colors, but the presets are the recommended path. Another pitfall: never invert the colors (light modules on dark background). While some scanners can handle inverted QR codes, many cannot. I considered adding an explicit warning when the user selects a light foreground, but decided instead to keep the color inputs labeled clearly — 'Foreground Color' and 'Background Color' — and trust that 'foreground = dark' is intuitive enough.

typescriptColor presets that guarantee scannability
const colorPresets = [
  { fg: "#000000", bg: "#ffffff", label: "Classic" },
  { fg: "#1e40af", bg: "#dbeafe", label: "Blue" },
  { fg: "#166534", bg: "#dcfce7", label: "Green" },
  { fg: "#9333ea", bg: "#f3e8ff", label: "Purple" },
  { fg: "#dc2626", bg: "#fef2f2", label: "Red" },
];
// Every preset maintains dark-on-light contrast > 4:1

Contrast Rule of Thumb

When designing QR code color presets, always pair a dark, saturated foreground with a very light tint of the same hue family. A deep blue (#1e40af) on light blue (#dbeafe) gives you a contrast ratio over 7:1 — well above the 4:1 minimum. Test every preset by scanning in dim lighting conditions, not just at your bright desk.

Try It NowQR Code GeneratorCreate custom QR codes for URLs, text, WiFi, and email — with color presets and dual export

SVG vs PNG — When to Use Which

Early in development I only offered PNG download, using QRCode.toDataURL() to generate a base64 image and triggering a download via a temporary anchor element. This works perfectly for sharing QR codes on social media, messaging apps, or embedding in documents. But users kept asking for SVG. The reason is obvious in hindsight: anyone printing QR codes on business cards, posters, or product packaging needs vector output. A 300x300 PNG looks fine on screen but becomes a blurry mess when printed at 2 inches wide on a 300 DPI business card. SVG scales infinitely because it stores vector paths, not pixels. Implementing SVG export required a different approach. I used QRCode.toString() with type 'svg', which returns the SVG markup as a string. Then I create a Blob with the MIME type 'image/svg+xml', generate an object URL, and trigger a download. One subtle detail: I call URL.revokeObjectURL() after the download to free the memory allocated for the blob URL. Without this, each SVG download leaks a small amount of memory. I also implemented a copy-to-clipboard feature using the Canvas API's toBlob() method combined with the Clipboard API's navigator.clipboard.write(). This only works with PNG (the Clipboard API doesn't support SVG), but it's incredibly convenient for users who want to paste a QR code directly into a presentation or document without saving a file first. The Clipboard API isn't supported in all browsers, so I wrapped it in a try-catch that silently fails on unsupported platforms.

typescriptSVG export implementation
const handleDownloadSVG = async () => {
  const content = getQRContent();
  if (!content) return;

  const svgString = await QRCode.toString(content, {
    type: "svg",
    width: size,
    margin: 2,
    color: { dark: fgColor, light: bgColor },
    errorCorrectionLevel: "M",
  });

  const blob = new Blob([svgString], { type: "image/svg+xml" });
  const url = URL.createObjectURL(blob);
  // Trigger download via temporary anchor element
};

Info

For the PNG download, I generate a Data URL from the canvas. For SVG, I use QRCode.toString() to get raw SVG markup, then create a Blob for download. Always call URL.revokeObjectURL() after triggering the download — otherwise each export leaks memory. It's a small amount per call, but it adds up in a single-page app where users might export dozens of codes.

Key Takeaway

The biggest lesson from this project: modern browser APIs are powerful enough for most use cases that developers reflexively reach for a server for. QR code generation, image manipulation via Canvas, file downloads via Blob URLs, clipboard access — it all runs natively in the browser with no server round-trip. The node-qrcode library was the right choice because it offers three output modes (Canvas, Data URL, SVG string) from a single, well-maintained package. If I were starting over, I'd make the same choice. The mistakes were educational too. Skipping special character escaping in WiFi mode taught me to always test with adversarial input. The color contrast issue taught me that QR codes are as much a physical medium as a digital one — they need to survive the real world of dim lighting and smudged screens. And the SVG export request taught me to always ask 'what will the user do with this output?' before deciding on a format. If you're building a similar tool, start with the qrcode npm package, default to error correction level M, offer both PNG and SVG, and — most importantly — test your WiFi QR codes on real phones. Emulators won't catch the edge cases.

Sources & Further Reading

Try the QR Code Generator

Generate QR codes for URLs, plain text, WiFi credentials, and email — all client-side with color customization and dual PNG/SVG export.

Open QR Code Generator

Related Articles