QR Code Generator with HTML, CSS & JavaScript

20 DAYS 20 PROJECT CHALLENGE

Day #12

Project Overview

A small QR Code Generator built with HTML, CSS and JavaScript that turns user text (URL, plain text, contact info, etc.) into a downloadable QR code image. This demo uses a third-party QR service (QRServer API) via fetch() to request generated QR images, and demonstrates building query parameters, handling blobs, showing previews, and offering a download. Great for learning how to integrate a simple external API and handle binary responses in the browser.

Key Features

  • Enter text (or pastes) and generate a QR code.
  • Configure size, margin, and error correction level (L / M / Q / H).
  • Shows a live preview.
  • Downloads generated QR as PNG.
  • Option to open the QR in a new tab.
  • Uses fetch() to retrieve the image as a Blob and displays it with URL.createObjectURL().
  • Graceful UI states: loading, invalid input, and error handling.

HTML Code

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Day 12 — QR Code Generator</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="card" role="main" aria-labelledby="title">
    <header>
      <div class="logo">QR</div>
      <div>
        <h1 id="title">Day 12: QR Code Generator</h1>
        <p class="lead">Generate and download QR codes from any text using the QRServer API and <code>fetch()</code>.</p>
      </div>
    </header>

    <section class="panel">
      <label class="field">
        <span class="label">Text or URL</span>
        <textarea id="inputText" placeholder="Enter a URL or text to convert to QR code" rows="3"></textarea>
      </label>

      <div class="row">
        <label class="field small">
          <span class="label">Size (px)</span>
          <input id="size" type="number" min="100" max="2000" value="300" />
        </label>

        <label class="field small">
          <span class="label">Margin (0-10)</span>
          <input id="margin" type="number" min="0" max="10" value="2" />
        </label>

        <label class="field small">
          <span class="label">Error correction</span>
          <select id="ecc">
            <option value="L">L (7%)</option>
            <option value="M" selected>M (15%)</option>
            <option value="Q">Q (25%)</option>
            <option value="H">H (30%)</option>
          </select>
        </label>
      </div>

      <div class="actions">
        <button id="generateBtn" class="btn">Generate QR</button>
        <button id="openBtn" class="btn secondary" disabled>Open in new tab</button>
        <button id="downloadBtn" class="btn secondary" disabled>Download PNG</button>
      </div>

      <div id="status" class="muted" aria-live="polite">Enter text and click <strong>Generate QR</strong>.</div>

      <div class="preview-wrap">
        <div id="preview" class="preview" aria-hidden="true">
          <!-- generated QR image will be inserted here -->
        </div>
      </div>

      <details style="margin-top:12px">
        <summary>How it works (short)</summary>
        <p class="small">The script builds a query URL (e.g. <code>https://api.qrserver.com/v1/create-qr-code/?size=300x300&amp;data=...&amp;ecc=M&amp;margin=2</code>), calls it with <code>fetch()</code> and receives the PNG image as a blob. We create an object URL and display it in an &lt;img&gt; tag and allow the user to download or open the image in a new tab.</p>
      </details>
    </section>
  </main>

  <script src="script.js"></script>
</body>
</html>

CSS Code

:root {
  --bg: #071026;
  --card: #0b1220;
  --accent: #7c3aed;
  --muted: #9aa4b2;
  --white: #e6eef6;
  font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: #002252;
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
}

.card {
  width: min(820px, 96%);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
  border-radius: 12px;
  padding: 18px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.03);
}

header {
  display: flex;
  gap: 12px;
  align-items: center;
}

.logo {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  background: linear-gradient(135deg, var(--accent), #22c1c3);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
}

h1 {
  margin: 0;
  font-size: 18px;
}

.lead {
  margin: 4px 0 12px;
  color: var(--muted);
  font-size: 14px;
}

.panel {
  margin-top: 12px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.field {
  display: flex;
  flex-direction: column;
}

.field .label {
  font-size: 13px;
  color: var(--muted);
  margin-bottom: 6px;
}

textarea#inputText {
  min-height: 64px;
  padding: 10px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.04);
  background: transparent;
  color: inherit;
  outline: none;
  resize: vertical;
}

input[type=number],
select {
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.04);
  background: transparent;
  color: inherit;
  width: 100%;
}

.row {
  display: flex;
  gap: 12px;
  align-items: end;
}

.row .small {
  flex: 1;
}

.actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.btn {
  padding: 10px 14px;
  border-radius: 10px;
  border: 0;
  background: linear-gradient(90deg, var(--accent), #22c1c3);
  color: white;
  font-weight: 600;
  cursor: pointer
}

.btn.secondary {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.06);
  color: var(--muted)
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed
}

.muted {
  color: var(--muted);
  font-size: 13px
}

.preview-wrap {
  display: flex;
  justify-content: center;
  margin-top: 12px;
}

.preview {
  width: 320px;
  height: 320px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.01), rgba(255, 255, 255, 0.005));
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.03);
}

.preview img {
  max-width: 100%;
  max-height: 100%;
  display: block;
  border-radius: 6px;
}

.small {
  font-size: 13px;
  color: var(--muted)
}

Javascript Code

// Day 12 — QR Code Generator using QRServer API and fetch()
// No API key required for QRServer. Example endpoint:
// https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=Hello&ecc=M&margin=2

// DOM
const inputText = document.getElementById('inputText');
const sizeInput = document.getElementById('size');
const marginInput = document.getElementById('margin');
const eccSelect = document.getElementById('ecc');

const generateBtn = document.getElementById('generateBtn');
const downloadBtn = document.getElementById('downloadBtn');
const openBtn = document.getElementById('openBtn');

const statusEl = document.getElementById('status');
const previewEl = document.getElementById('preview');

let currentObjectUrl = null; // track created object URL so we can revoke it

// Utility: build QRServer URL
function buildQrUrl({ data, size = 300, ecc = 'M', margin = 2 }) {
  const url = new URL('https://api.qrserver.com/v1/create-qr-code/');
  const px = Number(size) || 300;
  url.searchParams.set('size', `${px}x${px}`);
  url.searchParams.set('data', data || '');
  url.searchParams.set('ecc', ecc || 'M');
  url.searchParams.set('margin', String(margin || 0));
  // Optionally set format param if supported: url.searchParams.set('format','png');
  return url.toString();
}

// Show message
function setStatus(msg, isError = false) {
  statusEl.textContent = msg;
  statusEl.style.color = isError ? '#ffbaba' : '';
}

// Clean preview object URL
function revokeCurrentUrl() {
  if (currentObjectUrl) {
    URL.revokeObjectURL(currentObjectUrl);
    currentObjectUrl = null;
  }
  previewEl.innerHTML = '';
  downloadBtn.disabled = true;
  openBtn.disabled = true;
}

// Fetch QR image as blob and show preview
async function generateQr() {
  const data = inputText.value.trim();
  if (!data) {
    setStatus('Please enter text or URL to generate a QR code.', true);
    return;
  }

  const size = Math.max(100, Math.min(2000, Number(sizeInput.value) || 300));
  const margin = Math.max(0, Math.min(10, Number(marginInput.value) || 2));
  const ecc = eccSelect.value || 'M';

  const url = buildQrUrl({ data, size, ecc, margin });

  setStatus('Generating QR…');
  revokeCurrentUrl();

  try {
    // Fetch the image as a blob so we can offer download and open in tab
    const resp = await fetch(url, { cache: 'no-cache' });
    if (!resp.ok) throw new Error(`Network error ${resp.status}`);

    const blob = await resp.blob();
    // Defensive: ensure it is an image
    if (!blob.type.startsWith('image/')) {
      throw new Error('Invalid response (not an image).');
    }

    // Create object URL and show it
    currentObjectUrl = URL.createObjectURL(blob);
    const img = document.createElement('img');
    img.src = currentObjectUrl;
    img.alt = 'Generated QR code';
    img.onload = () => {
      setStatus('QR generated. You can download or open it in a new tab.');
    };
    img.onerror = () => {
      setStatus('Failed to load generated image.', true);
    };

    previewEl.appendChild(img);
    downloadBtn.disabled = false;
    openBtn.disabled = false;
  } catch (err) {
    console.error(err);
    setStatus('Failed to generate QR code. Try again.', true);
  }
}

// Download current image (as .png)
function downloadCurrentImage() {
  if (!currentObjectUrl) return;
  const link = document.createElement('a');
  link.href = currentObjectUrl;
  // create a filename from input (sanitize)
  const safe = inputText.value.trim().replace(/[^a-z0-9\-_.]/gi, '_').slice(0, 40) || 'qr';
  link.download = `${safe}.png`;
  document.body.appendChild(link);
  link.click();
  link.remove();
}

// Open in new tab
function openInNewTab() {
  if (!currentObjectUrl) return;
  window.open(currentObjectUrl, '_blank');
}

// Events
generateBtn.addEventListener('click', (e) => {
  e.preventDefault();
  generateQr();
});

downloadBtn.addEventListener('click', (e) => {
  e.preventDefault();
  downloadCurrentImage();
});

openBtn.addEventListener('click', (e) => {
  e.preventDefault();
  openInNewTab();
});

// Convenience: generate on Ctrl+Enter
inputText.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
    e.preventDefault();
    generateQr();
  }
});

// Cleanup on unload
window.addEventListener('beforeunload', () => {
  revokeCurrentUrl();
});
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Related Projects

Day 10 : Typing Speed Test

Measures typing speed and accuracy.

Concepts: Timers, string comparison, event listeners.

Day 14 : Image Gallery with Modal

Displays images in a grid with a pop-up modal view.

Concepts: DOM traversal, event bubbling.

Day 15 : Light/Dark Mode Toggle

Switch between light and dark themes with one click (and remember the choice).

Concepts: CSS variables, LocalStorage.