In today's interconnected world, QR codes have evolved from a novelty to an essential communication tool. Whether you're sharing contact info, linking to websites, or connecting social profiles, QR codes bridge our physical and digital experiences seamlessly.

In this guide, I'll walk you through building a comprehensive QR code application that both generates customized codes and scans existing ones. This project perfectly balances learning and practical utility! 💻

What We're Building

Before diving into code, check out the working application: QR Code Master

Our QR Hub will feature:

  • 🛠️ Generate QR codes for multiple content types:

    • Website URLs
    • Social media profiles
    • WhatsApp messages
    • Geographic coordinates
    • vCard contact information
  • ✨ Customize QR codes with:

    • Custom colors
    • Background colors
    • Size adjustments
    • Various design patterns
  • 📥 Export QR codes as:

    • JPG
    • PNG
    • SVG
    • PDF
  • 🔍 Decode QR codes from uploaded images

  • 🌙 Light/dark theme toggle

Let's start building!

Project Architecture

We'll create this application using HTML, CSS, and vanilla JavaScript, supplemented by three key libraries:

  1. QRCode.js: Generates our QR codes
  2. jsQR: Scans and decodes QR codes
  3. jsPDF: Creates PDF exports

Setting Up the HTML Structure

Let's begin with our HTML framework:

</span>
 lang="en">

     charset="UTF-8">
     name="viewport" content="width=device-width, initial-scale=1.0">
    QR Code Master
     rel="stylesheet" href="styles.css">
    <span class="na">src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js">
    <span class="na">src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js">
    <span class="na">src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js">


     class="container">
        
            QR Code Master
             id="theme-toggle">Toggle Theme
        

         class="main-content">
             class="generator-section">
                Generate QR Code
                 class="input-group">
                     id="qr-type">
                         value="url">Website URL
                         value="instagram">Instagram
                         value="facebook">Facebook
                         value="whatsapp">WhatsApp
                         value="twitter">Twitter
                         value="location">Location
                         value="vcard">vCard
                    
                     id="dynamic-fields">
                

                 class="customization">
                    QR Color:  type="color" id="qr-color" value="#000000">
                    BG Color:  type="color" id="qr-bg-color" value="#ffffff">
                    Size:  type="range" id="qr-size" min="100" max="1000" value="500">
                    Design: 
                         id="qr-design">
                             value="standard">Standard
                             value="high-contrast">High Contrast
                             value="minimal">Minimal
                             value="detailed">Detailed
                        
                    
                

                 id="generate-btn">Generate QR
                 id="qrcode" class="qr-preview">
                 id="download-options" class="hidden">
                     id="download-jpg">Download JPG
                     id="download-png">Download PNG
                     id="download-svg">Download SVG
                     id="download-pdf">Download PDF
                
            

             class="scanner-section">
                Scan QR Code
                 type="file" id="qr-upload" accept="image/*">
                 id="upload-scan" class="scan-btn">Scan Uploaded QR
                 id="scan-result" class="scan-result">
            
        
    
    <span class="na">src="script.js">





    Enter fullscreen mode
    


    Exit fullscreen mode
    




Our structure includes:
A header with app title and theme toggle
A generator section with customization options
A scanner section for uploading and decoding QR codes

  
  
  Styling Our Application
Next, let's add styling to create an intuitive, responsive interface:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

body {
    background: #f0f2f5;
    color: #333;
    transition: all 0.3s ease;
    min-height: 100vh;
    padding: 20px;
}

body.dark {
    background: #1a1a1a;
    color: #fff;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
}

header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 40px;
}

h1 {
    font-size: 2.5rem;
    background: linear-gradient(45deg, #007bff, #00ff95);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

#theme-toggle {
    padding: 10px 20px;
    border: none;
    border-radius: 25px;
    background: #007bff;
    color: white;
    cursor: pointer;
    transition: transform 0.2s;
}

#theme-toggle:hover {
    transform: scale(1.05);
}

.main-content {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 40px;
}

.generator-section, .scanner-section {
    background: white;
    padding: 30px;
    border-radius: 15px;
    box-shadow: 0 5px 15px rgba(0,0,0,0.1);
    transition: all 0.3s ease;
}

body.dark .generator-section,
body.dark .scanner-section {
    background: #2d2d2d;
}

h2 {
    margin-bottom: 20px;
    color: #007bff;
}

.input-group {
    margin-bottom: 20px;
}

select, input, textarea {
    width: 100%;
    padding: 12px;
    margin: 10px 0;
    border: 2px solid #ddd;
    border-radius: 8px;
    font-size: 1rem;
    transition: border-color 0.3s;
}

select:focus, input:focus, textarea:focus {
    border-color: #007bff;
    outline: none;
}

.customization {
    display: flex;
    gap: 15px;
    margin-bottom: 20px;
    align-items: center;
    flex-wrap: wrap;
}

.customization label {
    display: flex;
    align-items: center;
    gap: 8px;
}

input[type="color"] {
    width: 40px;
    height: 40px;
    padding: 2px;
    border: 2px solid #ddd;
    border-radius: 4px;
    cursor: pointer;
    background: none;
}

input[type="color"]:focus {
    border-color: #007bff;
}

#qr-design {
    width: auto;
    min-width: 120px;
}

button {
    padding: 12px 25px;
    border: none;
    border-radius: 8px;
    background: #007bff;
    color: white;
    cursor: pointer;
    transition: all 0.3s;
}

button:hover {
    background: #0056b3;
    transform: translateY(-2px);
}

.qr-preview {
    margin: 20px 0;
    text-align: center;
}

.scan-result {
    padding: 15px;
    background: #f8f9fa;
    border-radius: 8px;
    word-break: break-all;
    margin-top: 20px;
}

body.dark .scan-result {
    background: #3d3d3d;
}

.hidden {
    display: none;
}

.scan-btn {
    margin-top: 10px;
}

#qr-upload {
    display: block;
    margin: 10px 0;
}

#download-options {
    display: flex;
    gap: 10px;
    justify-content: center;
    margin-top: 20px;
}

#download-options button {
    padding: 10px 20px;
}

@media (max-width: 768px) {
    .main-content {
        grid-template-columns: 1fr;
    }
}



    Enter fullscreen mode
    


    Exit fullscreen mode
    




This CSS provides:
Responsive layout using CSS Grid
Smooth transitions between light and dark themes
Intuitive control styling and feedback
Consistent spacing and typography

  
  
  Adding Functionality with JavaScript
Now for the core functionality:

document.addEventListener('DOMContentLoaded', () => {

    if (typeof QRCode === 'undefined') {
        alert('QRCode.js failed to load. QR generation will not work.');
        return;
    }
    if (typeof jsQR === 'undefined') {
        alert('jsQR failed to load. QR scanning will not work.');
    }
    if (typeof jspdf === 'undefined') {
        alert('jsPDF failed to load. PDF download will not work.');
    }


    const themeToggle = document.getElementById('theme-toggle');
    themeToggle.addEventListener('click', () => {
        document.body.classList.toggle('dark');
    });


    const qrType = document.getElementById('qr-type');
    const dynamicFields = document.getElementById('dynamic-fields');
    const qrColor = document.getElementById('qr-color');
    const qrBgColor = document.getElementById('qr-bg-color');
    const qrSize = document.getElementById('qr-size');
    const qrDesign = document.getElementById('qr-design');
    const generateBtn = document.getElementById('generate-btn');
    const qrcodeDiv = document.getElementById('qrcode');
    const downloadOptions = document.getElementById('download-options');
    const downloadJpgBtn = document.getElementById('download-jpg');
    const downloadPngBtn = document.getElementById('download-png');
    const downloadSvgBtn = document.getElementById('download-svg');
    const downloadPdfBtn = document.getElementById('download-pdf');

    let qrCode;

    const fieldTemplates = {
        url: ``,
        instagram: ``,
        facebook: ``,
        whatsapp: `
            
            
        `,
        twitter: ``,
        location: `
            
            
        `,
        vcard: `
            
            
            
            
            
            
            
            
            
            
        `
    };

    function updateFields() {
        dynamicFields.innerHTML = fieldTemplates[qrType.value] || fieldTemplates.url;
    }

    qrType.addEventListener('change', updateFields);
    updateFields(); 

    generateBtn.addEventListener('click', () => {
        qrcodeDiv.innerHTML = '';
        let content = '';

        switch (qrType.value) {
            case 'url':
                content = document.getElementById('url-input')?.value;
                break;
            case 'instagram':
                const igUsername = document.getElementById('instagram-input')?.value;
                content = `https://instagram.com/${igUsername}`;
                break;
            case 'facebook':
                const fbUsername = document.getElementById('facebook-input')?.value;
                content = `https://facebook.com/${fbUsername}`;
                break;
            case 'whatsapp':
                const waNumber = document.getElementById('whatsapp-number')?.value;
                const waMessage = document.getElementById('whatsapp-message')?.value;
                content = `https://wa.me/${waNumber}${waMessage ? `?text=${encodeURIComponent(waMessage)}` : ''}`;
                break;
            case 'twitter':
                const twitterHandle = document.getElementById('twitter-input')?.value;
                content = `https://twitter.com/${twitterHandle}`;
                break;
            case 'location':
                const lat = document.getElementById('location-lat')?.value;
                const lng = document.getElementById('location-lng')?.value;
                content = `geo:${lat},${lng}`;
                break;
            case 'vcard':
                const firstname = document.getElementById('vcard-firstname')?.value;
                const lastname = document.getElementById('vcard-lastname')?.value;
                const role = document.getElementById('vcard-role')?.value || '';
                const company = document.getElementById('vcard-company')?.value || '';
                const website = document.getElementById('vcard-website')?.value || '';
                const email = document.getElementById('vcard-email')?.value;
                const address = document.getElementById('vcard-address')?.value || '';
                const phone = document.getElementById('vcard-phone')?.value;
                const fax = document.getElementById('vcard-fax')?.value || '';
                const notes = document.getElementById('vcard-notes')?.value || '';

                if (!firstname || !lastname || !email || !phone) {
                    alert('Please fill in all required vCard fields (First Name, Last Name, E-mail, Phone Number)');
                    return;
                }

                content = `BEGIN:VCARD\r\nVERSION:3.0\r\nN:${lastname};${firstname};;;\r\nFN:${firstname} ${lastname}\r\nTITLE:${role}\r\nORG:${company}\r\nURL:${website}\r\nEMAIL:${email}\r\nADR:;;${address};;;;\r\nTEL:${phone}\r\nFAX:${fax}\r\nNOTE:${notes}\r\nEND:VCARD`;
                break;
        }

        if (content) {
            generateQR(content);
        } else {
            alert('Please provide valid input to generate a QR code.');
        }
    });

    function generateQR(content) {
        try {

            const designSettings = {
                standard: QRCode.CorrectLevel.M,
                'high-contrast': QRCode.CorrectLevel.H,
                minimal: QRCode.CorrectLevel.L,
                detailed: QRCode.CorrectLevel.Q
            };

            qrCode = new QRCode(qrcodeDiv, {
                text: content,
                width: parseInt(qrSize.value),
                height: parseInt(qrSize.value),
                colorDark: qrColor.value,
                colorLight: qrBgColor.value, 
                correctLevel: designSettings[qrDesign.value] || QRCode.CorrectLevel.M
            });
            downloadOptions.classList.remove('hidden');
        } catch (error) {
            console.error('Error generating QR code:', error);
            alert('An error occurred while generating the QR code.');
        }
    }


    function downloadImage(format) {
        const qrCanvas = qrcodeDiv.querySelector('canvas');
        if (!qrCanvas) return;

        const link = document.createElement('a');
        if (format === 'jpg') {
            link.download = 'qrcode.jpg';
            link.href = qrCanvas.toDataURL('image/jpeg', 1.0);
        } else if (format === 'png') {
            link.download = 'qrcode.png';
            link.href = qrCanvas.toDataURL('image/png');
        }
        link.click();
    }

    downloadJpgBtn.addEventListener('click', () => downloadImage('jpg'));
    downloadPngBtn.addEventListener('click', () => downloadImage('png'));

    downloadSvgBtn.addEventListener('click', () => {
        const qrCanvas = qrcodeDiv.querySelector('canvas');
        if (!qrCanvas) return;
        const svgSize = parseInt(qrSize.value);
        const svg = `${svgSize}" height="${svgSize}" viewBox="0 0 ${svgSize} ${svgSize}">
            ${qrCanvas.toDataURL('image/png')}" width="${svgSize}" height="${svgSize}"/>
        `;
        const blob = new Blob([svg], { type: 'image/svg+xml' });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.download = 'qrcode.svg';
        link.href = url;
        link.click();
        URL.revokeObjectURL(url);
    });

    downloadPdfBtn.addEventListener('click', () => {
        if (typeof jspdf === 'undefined') {
            alert('PDF functionality is unavailable due to jsPDF not loading.');
            return;
        }
        const qrCanvas = qrcodeDiv.querySelector('canvas');
        if (!qrCanvas) return;

        const { jsPDF } = window.jspdf;
        const pdf = new jsPDF();
        const imgData = qrCanvas.toDataURL('image/png');
        const imgProps = pdf.getImageProperties(imgData);
        const pdfWidth = pdf.internal.pageSize.getWidth();
        const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width;
        pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight);
        pdf.save('qrcode.pdf');
    });


    const qrUpload = document.getElementById('qr-upload');
    const uploadScanBtn = document.getElementById('upload-scan');
    const scanResult = document.getElementById('scan-result');

    if (typeof jsQR !== 'undefined') {
        uploadScanBtn.addEventListener('click', () => {
            const file = qrUpload.files[0];
            if (!file) {
                scanResult.textContent = 'Please select an image file containing a QR code.';
                return;
            }

            scanResult.textContent = 'Scanning...';

            const reader = new FileReader();
            reader.onload = function(e) {
                const img = new Image();
                img.onload = function() {
                    const canvas = document.createElement('canvas');
                    const context = canvas.getContext('2d');
                    canvas.width = img.width;
                    canvas.height = img.height;
                    context.drawImage(img, 0, 0, img.width, img.height);

                    const imageData = context.getImageData(0, 0, img.width, img.height);
                    const code = jsQR(imageData.data, imageData.width, imageData.height);

                    if (code) {
                        scanResult.textContent = code.data;
                    } else {
                        scanResult.textContent = 'Unable to decode QR code from the uploaded image. Please ensure it’s a valid QR code.';
                    }
                };
                img.src = e.target.result;
            };
            reader.readAsDataURL(file);
        });
    } else {
        uploadScanBtn.disabled = true;
        scanResult.textContent = 'QR scanning is unavailable due to library loading issues.';
    }
});



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  Breaking Down the Key Features

  
  
  Dynamic Form Generation
One of the most powerful aspects of our app is how it adapts to different QR code types. When a user selects "WhatsApp," they'll see fields for phone numbers and messages. For "vCard," they'll get contact information fields.This adaptability comes from dynamically swapping form templates based on user selection:

// Example of form swapping logic (will be in the main JS code)
function updateFields() {
    dynamicFields.innerHTML = fieldTemplates[qrType.value] || fieldTemplates.url;
}

qrType.addEventListener('change', updateFields);



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  QR Code Generation and Customization
Our generation system handles different content types and reformats input appropriately. For example, when creating a vCard QR code, we format the input following the vCard standard:

// Example vCard formatting (will be in the main JS code)
content = `BEGIN:VCARD\r\nVERSION:3.0\r\nN:${lastname};${firstname};;;\r\nFN:${firstname} ${lastname}\r\n...`;



    Enter fullscreen mode
    


    Exit fullscreen mode
    




Users can select from four design complexity levels, each mapping to a different error correction level in the QRCode library:
Standard (Level M)
High Contrast (Level H)
Minimal (Level L)
Detailed (Level Q)
These settings affect both the QR code's appearance and its scanning reliability.
  
  
  Multi-Format Export Options
After generating a QR code, users can download it in several formats:

// Example download function (will be in the main JS code)
function downloadImage(format) {
    const qrCanvas = qrcodeDiv.querySelector('canvas');
    if (!qrCanvas) return;

    const link = document.createElement('a');
    // Set appropriate MIME type and filename based on format
    // ...
    link.click();
}



    Enter fullscreen mode
    


    Exit fullscreen mode
    




For SVG and PDF exports, we do additional processing to convert the canvas data into the appropriate format.
  
  
  QR Code Scanning
The scanning feature allows users to upload images containing QR codes and decode them:

// QR scanning logic (will be in main JS)
reader.onload = function(e) {
    // Create image from file
    // Draw image to canvas
    // Extract image data
    // Decode with jsQR
    // Display results
};



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  Themes and UI Enhancements
The theme toggle function adds visual flexibility:

// Theme toggle (will be in main JS)
themeToggle.addEventListener('click', () => {
    document.body.classList.toggle('dark');
});



    Enter fullscreen mode
    


    Exit fullscreen mode
    




This simple feature greatly improves usability in different lighting conditions.
  
  
  Error Handling and Validation
Our app includes several safeguards:
Library availability checks on load
Required field validation for complex types like vCard
Try-catch blocks around QR generation to handle errors gracefully

  
  
  Extending Your QR Hub
Once you've built the core application, consider these enhancements:

Live Camera Scanning: Implement webcam integration for real-time scanning

Logo Embedding: Allow users to add logos in the center of QR codes

Batch Generation: Create multiple QR codes from data sets

QR Code History: Save previously generated codes

Design Templates: Add pre-configured style combinations

  
  
  Putting It All Together
This project demonstrates many core web development concepts:
Working with third-party libraries
Dynamic DOM manipulation
Canvas operations
File handling
Custom theming
Responsive design
The resulting application is both practical and educational—exactly what makes for a great dev portfolio project.
  
  
  Conclusion
Building this QR code hub showcases the power of combining existing libraries with custom functionality to create something genuinely useful. The next time you need to share information quickly, you'll have your own personalized tool ready to go! 🚀The complete project is available at: QR Code MasterHave you built similar tools or have ideas for extending this project? Share your thoughts in the comments!What aspects of this project do you find most interesting? I'd love to hear which features you'd like to see expanded in future tutorials!