What You'll Build

A Base64 encoder/decoder that handles both text and files. Paste text or drop a file — get Base64 instantly. Switch to decode mode — paste Base64 and get the original back. Includes URL-safe mode and UTF-8 support for emojis and non-English text.

  • Encode mode: text → Base64
  • Decode mode: Base64 → text
  • File support: drag & drop any file → Base64
  • URL-safe toggle (replaces + / with - _)
  • UTF-8 mode for emojis and Unicode text
  • Copy output with one click
▶ Live Preview — this is what you'll build

What You'll Learn

Warning upfront: Base64 is not encryption. Anyone can decode it. This tool is for encoding data to send through text channels, not for protecting secrets.

1

Build the HTML Structure

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Base64 Encoder</title>
</head>
<body>
    <div class="card">
        <h1>Base64 Encoder / Decoder</h1>
        <p class="sub">Text and file encoding - runs in your browser</p>

        <div class="modes">
            <button class="mode active" data-mode="encode">Encode</button>
            <button class="mode" data-mode="decode">Decode</button>
        </div>

        <div class="options">
            <button class="option" id="urlSafeBtn">URL-safe</button>
            <button class="option active" id="utf8Btn">UTF-8</button>
        </div>

        <div class="field">
            <label id="inputLabel">Input Text</label>
            <textarea id="input" spellcheck="false">Hello from MAK Studio!</textarea>
        </div>

        <div class="file-drop" id="fileDrop">
            Drop a file here or click to browse
        </div>
        <input type="file" id="fileInput" style="display:none;">
        <div class="file-info" id="fileInfo"></div>

        <div class="error" id="errorMsg"></div>

        <div class="actions">
            <button class="btn primary" id="runBtn">Encode</button>
            <button class="btn ghost" id="copyBtn">Copy</button>
            <button class="btn ghost" id="clearBtn">Clear</button>
        </div>

        <label>Output</label>
        <div class="output" id="output"></div>

        <div class="stats">
            <span>Input: <strong id="inLen">0</strong> chars</span>
            <span>Output: <strong id="outLen">0</strong> chars</span>
            <span>Ratio: <strong id="ratio">—</strong></span>
        </div>
    </div>

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

Style It with CSS

CSS
* { box-sizing: border-box; margin: 0; padding: 0; }

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: #f1f5f9;
    padding: 20px;
    color: #0f172a;
}

.card {
    max-width: 560px;
    margin: 0 auto;
    background: #fff;
    border-radius: 16px;
    padding: 24px;
    box-shadow: 0 10px 40px rgba(15,23,42,0.08);
}

h1 { font-size: 1.3rem; margin-bottom: 4px; }
.sub { color: #64748b; font-size: 0.85rem; margin-bottom: 16px; }

/* Encode/Decode mode buttons */
.modes {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 6px;
    margin-bottom: 12px;
}
.mode {
    background: #f8fafc;
    border: 2px solid #e2e8f0;
    color: #64748b;
    padding: 10px;
    border-radius: 10px;
    font-weight: 700;
    font-size: 0.85rem;
    cursor: pointer;
    transition: 0.2s;
    font-family: inherit;
}
.mode:hover { border-color: #cbd5e1; color: #334155; }
.mode.active {
    background: #eff6ff;
    border-color: #3b82f6;
    color: #1d4ed8;
}

/* URL-safe + UTF-8 toggles */
.options {
    display: flex;
    gap: 6px;
    margin-bottom: 12px;
    flex-wrap: wrap;
}
.option {
    background: #f8fafc;
    border: 2px solid #e2e8f0;
    color: #64748b;
    padding: 5px 10px;
    border-radius: 6px;
    font-weight: 700;
    font-size: 0.72rem;
    cursor: pointer;
    transition: 0.2s;
    font-family: inherit;
}
.option.active {
    background: #eff6ff;
    border-color: #3b82f6;
    color: #1d4ed8;
}

/* Input textarea */
.field { margin-bottom: 10px; }
label {
    display: block;
    font-size: 0.75rem;
    font-weight: 600;
    color: #334155;
    margin-bottom: 4px;
}
textarea {
    width: 100%;
    min-height: 100px;
    padding: 10px 12px;
    border: 2px solid #e2e8f0;
    border-radius: 10px;
    font-size: 0.82rem;
    font-family: "Courier New", monospace;
    outline: none;
    resize: vertical;
    line-height: 1.5;
    word-break: break-all;
}
textarea:focus {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}

/* File drop zone */
.file-drop {
    border: 2px dashed #cbd5e1;
    border-radius: 10px;
    padding: 18px;
    text-align: center;
    background: #f8fafc;
    cursor: pointer;
    transition: 0.2s;
    margin-bottom: 10px;
    font-size: 0.82rem;
    color: #64748b;
}
.file-drop:hover, .file-drop.over {
    border-color: #3b82f6;
    background: #eff6ff;
    color: #1d4ed8;
}

.file-info {
    background: #f0fdf4;
    border: 1px solid #86efac;
    color: #166534;
    padding: 8px 12px;
    border-radius: 8px;
    font-size: 0.78rem;
    margin-bottom: 10px;
    display: none;
}
.file-info.show { display: block; }

/* Toolbar */
.actions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 8px 0;
}
.btn {
    border: none;
    border-radius: 8px;
    padding: 9px 14px;
    font-weight: 700;
    font-size: 0.8rem;
    cursor: pointer;
    transition: 0.2s;
    font-family: inherit;
}
.btn.primary { background: #3b82f6; color: #fff; }
.btn.primary:hover { background: #2563eb; }
.btn.green { background: #10b981; color: #fff; }
.btn.green:hover { background: #059669; }
.btn.ghost { background: #f1f5f9; color: #334155; }
.btn.ghost:hover { background: #e2e8f0; }

/* Error + output */
.error {
    background: #fef2f2;
    border: 1px solid #fca5a5;
    color: #dc2626;
    padding: 10px 12px;
    border-radius: 8px;
    font-size: 0.78rem;
    font-family: "Courier New", monospace;
    margin: 8px 0;
    display: none;
    word-break: break-word;
}
.error.show { display: block; }

.output {
    background: #0f172a;
    border-radius: 10px;
    padding: 14px;
    color: #e2e8f0;
    font-family: "Courier New", monospace;
    font-size: 0.78rem;
    white-space: pre-wrap;
    word-break: break-all;
    max-height: 220px;
    overflow-y: auto;
    line-height: 1.5;
    min-height: 80px;
}
.output:empty::before {
    content: "Output will appear here...";
    color: #64748b;
    font-style: italic;
    word-break: normal;
}

/* Stats */
.stats {
    display: flex;
    gap: 12px;
    font-size: 0.72rem;
    color: #64748b;
    margin: 8px 0;
    flex-wrap: wrap;
}
.stats strong { color: #334155; font-family: "Courier New", monospace; }
3

Write the JavaScript — Base64 Core

JavaScript — Part 1: Core
// ============================================================
// STATE
// ============================================================

var mode = "encode";
var urlSafe = false;
var utf8 = true;
var fileData = null;

var inputEl = document.getElementById("input");
var outputEl = document.getElementById("output");
var errorEl = document.getElementById("errorMsg");

// ============================================================
// BASE64 CORE FUNCTIONS
// ============================================================

// Convert an array of bytes to a Base64 string.
// Chunked to avoid "Maximum call stack size exceeded" on
// large files — String.fromCharCode.apply() has a limit.
function arrayToBase64(bytes, safe) {
    var binary = "";
    var chunk = 0x8000;  // 32KB per chunk
    for (var i = 0; i < bytes.length; i += chunk) {
        binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
    }
    var b64 = btoa(binary);
    if (safe) {
        b64 = b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
    }
    return b64;
}

// Encode a text string to Base64.
// UTF-8 mode uses TextEncoder so emojis and non-English
// characters work correctly.
function encodeText(str, useUtf8, safe) {
    var b64;
    if (useUtf8) {
        var bytes = new TextEncoder().encode(str);
        var binary = "";
        var chunk = 0x8000;
        for (var i = 0; i < bytes.length; i += chunk) {
            binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
        }
        b64 = btoa(binary);
    } else {
        b64 = btoa(str);
    }
    if (safe) {
        b64 = b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
    }
    return b64;
}

// Decode a Base64 string back to text.
// Handles URL-safe input and re-adds padding if missing.
function decodeText(str, useUtf8) {
    var clean = str.replace(/\s+/g, "");
    var hasUrlSafe = /[-_]/.test(clean);

    if (hasUrlSafe) {
        clean = clean.replace(/-/g, "+").replace(/_/g, "/");
    }
    // Add padding back if it was stripped
    while (clean.length % 4 !== 0) clean += "=";

    // Validate characters before calling atob()
    if (!/^[A-Za-z0-9+/]*={0,2}$/.test(clean)) {
        throw new Error("contains characters that are not valid Base64");
    }

    var binary = atob(clean);

    if (useUtf8) {
        var bytes = new Uint8Array(binary.length);
        for (var i = 0; i < binary.length; i++) {
            bytes[i] = binary.charCodeAt(i);
        }
        return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
    }
    return binary;
}

Why chunk at 32KB? String.fromCharCode.apply(null, array) is fast, but fails on very large arrays (browser stack limit is around 65K arguments). Chunking in 32KB pieces keeps us safely under the limit even for multi-MB files.

Why UTF-8 matters: btoa("🚀") throws an error — it only handles Latin-1 characters. To encode emojis or non-English text, we use TextEncoder to convert the string to UTF-8 bytes first, then base64-encode those bytes.

4

Write the JavaScript — File Handling

JavaScript — Part 2: Files
// ============================================================
// FILE HANDLING
// ============================================================
// FileReader reads the file as an ArrayBuffer (raw bytes).
// We then feed those bytes straight into arrayToBase64().

var fileDrop = document.getElementById("fileDrop");
var fileInput = document.getElementById("fileInput");
var fileInfo = document.getElementById("fileInfo");

// Click to browse
fileDrop.addEventListener("click", function() { fileInput.click(); });

// Drag and drop
fileDrop.addEventListener("dragover", function(e) {
    e.preventDefault();
    fileDrop.classList.add("over");
});
fileDrop.addEventListener("dragleave", function() {
    fileDrop.classList.remove("over");
});
fileDrop.addEventListener("drop", function(e) {
    e.preventDefault();
    fileDrop.classList.remove("over");
    if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
});

// File input change
fileInput.addEventListener("change", function(e) {
    if (e.target.files.length) handleFile(e.target.files[0]);
});

function handleFile(file) {
    if (mode !== "encode") return;

    var reader = new FileReader();

    reader.onload = function(e) {
        var bytes = new Uint8Array(e.target.result);
        var b64 = arrayToBase64(bytes, urlSafe);

        fileData = { name: file.name, size: file.size };
        fileInfo.textContent = "📎 " + file.name + " (" + formatBytes(file.size) + ")";
        fileInfo.classList.add("show");

        outputEl.textContent = b64;
        updateStats();
        errorEl.classList.remove("show");
    };

    reader.onerror = function() {
        errorEl.textContent = "❌ Failed to read file.";
        errorEl.classList.add("show");
    };

    // Read as ArrayBuffer = raw bytes
    reader.readAsArrayBuffer(file);
}
5

Write the JavaScript — Run & Events

JavaScript — Part 3: Events
// ============================================================
// RUN — encode or decode based on current mode
// ============================================================

function run() {
    errorEl.classList.remove("show");
    var text = inputEl.value;

    if (!text) {
        outputEl.textContent = "";
        updateStats();
        return;
    }

    try {
        var result;
        if (mode === "encode") {
            result = encodeText(text, utf8, urlSafe);
        } else {
            result = decodeText(text, utf8);
        }
        outputEl.textContent = result;
    } catch (e) {
        errorEl.textContent = "❌ " + e.message;
        errorEl.classList.add("show");
        outputEl.textContent = "";
    }
    updateStats();
}

// ============================================================
// STATS
// ============================================================

function updateStats() {
    var inLen = inputEl.value.length;
    var outLen = outputEl.textContent.length;
    document.getElementById("inLen").textContent = inLen.toLocaleString();
    document.getElementById("outLen").textContent = outLen.toLocaleString();

    var ratioEl = document.getElementById("ratio");
    if (inLen === 0 || outLen === 0) {
        ratioEl.textContent = "—";
    } else {
        ratioEl.textContent = ((outLen / inLen) * 100).toFixed(1) + "%";
    }
}

function formatBytes(b) {
    if (b === 0) return "0 B";
    var k = 1024;
    var sizes = ["B", "KB", "MB", "GB"];
    var i = Math.floor(Math.log(b) / Math.log(k));
    return parseFloat((b / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
}

// ============================================================
// MODE SWITCH
// ============================================================

document.querySelectorAll(".mode").forEach(function(btn) {
    btn.addEventListener("click", function() {
        document.querySelectorAll(".mode").forEach(function(b) {
            b.classList.remove("active");
        });
        btn.classList.add("active");
        mode = btn.dataset.mode;

        var label = document.getElementById("inputLabel");
        var runBtn = document.getElementById("runBtn");

        if (mode === "encode") {
            label.textContent = "Input Text";
            runBtn.textContent = "▶️ Encode";
            runBtn.className = "btn primary";
            fileDrop.style.display = "block";
        } else {
            label.textContent = "Base64 Input";
            runBtn.textContent = "▶️ Decode";
            runBtn.className = "btn green";
            fileDrop.style.display = "none";
            fileData = null;
            fileInfo.classList.remove("show");
        }
        outputEl.textContent = "";
        errorEl.classList.remove("show");
        updateStats();
    });
});

// ============================================================
// OPTION TOGGLES
// ============================================================

document.getElementById("urlSafeBtn").addEventListener("click", function() {
    urlSafe = !urlSafe;
    this.classList.toggle("active", urlSafe);
    run();
});

document.getElementById("utf8Btn").addEventListener("click", function() {
    utf8 = !utf8;
    this.classList.toggle("active", utf8);
    run();
});

// ============================================================
// ACTION BUTTONS
// ============================================================

document.getElementById("runBtn").addEventListener("click", run);

document.getElementById("copyBtn").addEventListener("click", function() {
    if (!outputEl.textContent) return;
    navigator.clipboard.writeText(outputEl.textContent).then(function() {
        var btn = document.getElementById("copyBtn");
        var orig = btn.textContent;
        btn.textContent = "✅ Copied!";
        setTimeout(function() { btn.textContent = orig; }, 1500);
    });
});

document.getElementById("clearBtn").addEventListener("click", function() {
    inputEl.value = "";
    outputEl.textContent = "";
    errorEl.classList.remove("show");
    fileData = null;
    fileInfo.classList.remove("show");
    updateStats();
});

// Live update
inputEl.addEventListener("input", run);

// Boot
updateStats();
run();

Save and open index.html. Type text → see Base64. Paste Base64 in Decode mode → get text back. Drop a file → get its Base64. Toggle URL-safe → see + and / become - and _.

6

Understand the Big Ideas

1. Base64 is not encryption

Anyone can decode Base64 with one line of code or a browser DevTools console. Never use it to hide passwords, API keys, or sensitive data. It's designed for encoding — making binary data safe for text-only channels.

2. Why btoa() fails on emojis

btoa("🚀") throws "InvalidCharacterError" — because 🚀 is not a Latin-1 character. btoa was designed before Unicode mattered. The fix:

JavaScript
// Wrong — throws on emoji
btoa("🚀");  // ❌ InvalidCharacterError

// Right — converts to UTF-8 bytes first
var bytes = new TextEncoder().encode("🚀");
// bytes = Uint8Array [240, 159, 154, 128]
var binary = String.fromCharCode(...bytes);
btoa(binary);  // ✅ "8J+agA=="

3. The 33% size increase

Base64 encodes 3 bytes into 4 characters — hence the ~33% overhead. That's the price of fitting arbitrary binary data into a text-only channel. It's why inline images in CSS use Base64 sparingly — small icons only.

4. FileReader + Uint8Array

FileReader.readAsArrayBuffer() gives us the raw bytes of any file (image, PDF, video, binary data). Wrapping it in new Uint8Array() gives us an array-like view we can iterate. Then we convert chunk by chunk into a binary string and feed it to btoa().

Real-world example: When you drag a photo into a form, and the server accepts it as a Base64 string, this is exactly the pipeline that happens in the browser.

5. Why URL-safe Base64 exists

Standard Base64 uses + and /. In URLs, + means "space" and / is a path separator. JWTs and API tokens use URL-safe Base64 (- and _ instead) to avoid these conflicts.

7

Practice Challenges

🟢 Beginner

🟡 Intermediate

🔴 Advanced

Compare your solution: We built the production version with all these features — see the live Base64 Encoder.

Full Source Code

Common Mistakes & Fixes

❌ "btoa throws on emojis or accented letters"

You forgot UTF-8 mode. Use new TextEncoder().encode(str) to get the bytes first, then base64-encode those bytes.

❌ "Decoding fails with invalid character error"

The input has URL-safe characters (-, _) but you haven't converted them back to +, /. Our decoder handles this automatically.

❌ "Padding issues — length not multiple of 4"

Some Base64 strings are missing their trailing =. Our decoder re-adds padding before calling atob().

❌ "Large file crashes the browser"

String.fromCharCode.apply() fails with large arrays. The chunked loop (32KB at a time) prevents this. For very large files (100MB+), you'd need a web worker and streaming — out of scope for this project.

❌ "I thought Base64 was encryption"

It is not. Base64 is encoding. Anyone can decode it. Use AES, RSA, or another real encryption scheme when you actually need to protect data.

What to Build Next