# Base64 Reference
Base64 turns arbitrary binary data into a string of 64 printable ASCII characters, so it can safely travel through text-only channels — email bodies, JSON fields, URLs, data: URIs. It is an encoding, not encryption: anyone can decode it, instantly, with no key.
| Index range | Characters |
|---|---|
| 0–25 | `A`–`Z` |
| 26–51 | `a`–`z` |
| 52–61 | `0`–`9` |
| 62 | `+` (standard) or `-` (URL-safe) |
| 63 | `/` (standard) or `_` (URL-safe) |
| padding | `=` |
Base64 groups input into 3-byte (24-bit) chunks and re-slices each chunk into four 6-bit values (0–63), each mapped to one alphabet character. 6 bits per output character is why the encoded size is always ~4/3 the size of the input (a rough rule: ceil(n / 3) * 4 bytes).
| Input bytes (mod 3) | Output ends with |
|---|---|
| 0 remainder | No padding |
| 1 remainder | `==` (two pad chars) |
| 2 remainder | `=` (one pad char) |
"M" (1 byte) -> "TQ=="
"Ma" (2 bytes) -> "TWE="
"Man" (3 bytes) -> "TWFu"
| Standard | URL-safe | |
|---|---|---|
| Char 62 | `+` | `-` |
| Char 63 | `/` | `_` |
| Padding | `=` required | Usually omitted |
URL-safe Base64 exists because + and / are meaningful in URLs and query strings (+ means space in form-encoded data; / is a path separator) — swapping them avoids percent-encoding the Base64 output a second time. JWTs use unpadded URL-safe Base64 for exactly this reason (see the related tools below to decode one).
// Node.js
Buffer.from('Hello, World!').toString('base64'); // "SGVsbG8sIFdvcmxkIQ=="
Buffer.from('SGVsbG8sIFdvcmxkIQ==', 'base64').toString(); // "Hello, World!"
// Browser (ASCII-safe strings only — see pitfall below)
btoa('Hello'); // "SGVsbG8="
atob('SGVsbG8=');// "Hello"
Buffer.from('Hello, World!').toString('base64url');
// "SGVsbG8sIFdvcmxkIQ" -- note: no padding, +/ replaced
function b64EncodeUnicode(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
(_, p1) => String.fromCharCode(parseInt(p1, 16))));
}
b64EncodeUnicode('café'); // "Y2Fmw6k="
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...">
btoa()/atob() only understand characters 0–255, and silently corrupt some Unicode instead of erroring: btoa('😀') or btoa('中文') throw InvalidCharacterError because those code points are above 255 — but a character like é (U+00E9) is within that range, so btoa('café') doesn't throw at all, it just encodes the wrong bytes ("Y2Fm6Q==", silently different from the correct UTF-8-aware result). Don't treat "it didn't throw" as "it worked" — always encode to UTF-8 bytes first (see the pattern above) or use TextEncoder + a byte-to-Base64 loop.= padding and reject unpadded input; others (Node's Buffer, most URL-safe decoders) are lenient. When round-tripping between systems, don't assume the receiver tolerates missing padding.base64 CLI and some legacy MIME encoders insert a newline every 76 characters (RFC 2045). Passing that output straight into JSON.parse or a URL will silently corrupt it — strip whitespace first, or pass -w0 to base64 on Linux to disable wrapping.data: URI, for example) this is a real bandwidth and parse-time cost, not just a formatting detail — weigh it against just linking to a binary asset.+// collide with in URLs.