Skip to content

Base64 Encode & Decode: Convert Text, Images, and Files Online

Free Base64 encoder/decoder with full Unicode support — standard vs. URL-safe variants explained, plus file-to-data-URI conversion. Runs entirely client-side.

Try it now: Base64 Encoder & Decoder Encode and decode Base64 and Base64URL with full Unicode support, plus file-to-data-URI conversion for images. Runs offline — nothing is ever uploaded.

What Base64 Actually Does

Base64 is an encoding, not encryption and not compression. It takes arbitrary binary data — bytes that can be anything from 0 to 255 — and re-expresses it using only 64 characters that are safe to put in plain text: A-Z, a-z, 0-9, plus + and /, with = used as trailing padding. Every 3 raw bytes become 4 base64 characters, which is why base64 always makes data roughly 33% larger — it never shrinks anything. The entire point is transport, not storage or secrecy: base64 exists so binary data can survive channels that were only ever designed to carry text — email (the original reason it was standardized), JSON string values, XML, and URLs.

The misconception worth correcting directly: base64 provides zeroconfidentiality. Encoding something as base64 is not a form of security — it's a reversible, publicly-documented mapping with no key. Anyone can decode it instantly, by pasting it into any base64-to-text tool or running a single line of code. If a JWT, an API token, or a password shows up as base64 somewhere, treat it as plaintext, because functionally it is.

Base64 vs. Base64URL: Why the Variant Exists

Standard base64's alphabet includes + and /, and both characters already mean something specific in a URL — + is historically interpreted as a space in query strings, and / is a path separator. Drop a standard base64 string into a URL unescaped and you risk silently mangling it. Base64url solves this by swapping those two characters: + becomes - (hyphen) and / becomes _ (underscore), both of which are inert in a URL. Base64url also typically omits the = padding entirely, since the length can be inferred and trailing = characters are themselves awkward in a query string or file name.

This is exactly why JSON Web Tokens use base64url instead of standard base64: a JWT is three base64url segments — header, payload, signature — joined with dots, and that whole string is routinely passed around as a URL query parameter or an HTTP header value, where a stray + or /would need extra escaping. See the JWT Decoder guide for how those three segments are structured once decoded.

same 2 raw bytes (0xFB 0xFF), two alphabets
raw bytes:        FB FF

standard base64:  +/8=
base64url:        -_8

Same underlying bytes, same bit pattern — only the character substitution and the padding change. Decoding either one back to text or bytes has to know which alphabet was used; feeding a base64url string into a strict standard-base64 decoder (or vice versa) fails on the first -, _, +, or /it doesn't expect.

The UTF-8 Trap: btoa() and atob() in JavaScript

JavaScript ships built-in btoa() and atob() functions, and they are the source of a genuinely common bug. btoa()only handles strings where every character's UTF-16 code unit fits in a single byte (0–255) — it treats the string as Latin-1, one code unit per byte. Encode plain ASCII and it works fine. Encode anything with a multi-byte UTF-8 character — accented letters, non-Latin scripts, emoji — and one of two things happens: it throws an error outright, or worse, it runs without error and silently produces the wrong bytes.

btoa() on multi-byte text — corrupts silently or throws
btoa("café")
// → "Y2Fm6Q=="   (WRONG — encodes é's raw UTF-16 unit as one byte, not its 2-byte UTF-8 form)

btoa("👍")
// → Uncaught InvalidCharacterError:
//    Failed to execute 'btoa': the string to be encoded contains characters
//    outside of the Latin1 range.

The correct base64 of the UTF-8 bytes for "café" is Y2Fmw6k=, not Y2Fm6Q==btoa() never sees the 2-byte UTF-8 encoding of é (0xC3 0xA9); it just grabs the character's raw UTF-16 code unit (233) and treats it as one byte. The fix is to route the string through TextEncoder first, which produces real UTF-8 bytes, then base64-encode those bytes directly instead of the JavaScript string:

the correct pattern — UTF-8 bytes first, then base64
function toBase64(str) {
  const bytes = new TextEncoder().encode(str);      // real UTF-8 bytes
  return btoa(String.fromCharCode(...bytes));        // now safe to pass to btoa
}

function fromBase64(b64) {
  const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);             // decodes UTF-8 correctly
}

toBase64("café") // → "Y2Fmw6k="

Skipping this step is the single most common reason a hand-rolled base64-to-text conversion works in testing (ASCII input) and then breaks in production the moment a user pastes an emoji or a name with an accent.

File to Base64: Embedding Assets as Data URIs

Converting a file to base64 is how a binary asset — most commonly a small image or icon — gets embedded directly inside HTML, CSS, or JSON as a data: URI, instead of being fetched over a separate HTTP request: data:image/png;base64,iVBORw0KG.... That trade is worth making for genuinely small, frequently-inlined assets (a favicon, a tiny sprite, an SVG icon) where saving a round trip matters more than the file's size.

The trade-off runs in both directions, and it's worth being explicit about it:

  • The file gets ~33% larger.Base64's per-character overhead applies to any file it touches — a 30 KB image becomes roughly 40 KB of base64 text.
  • The browser can't cache it separately.An inlined data URI lives inside the HTML or CSS that references it, so it's re-downloaded every time that parent file is, with none of the independent caching a normal image URL gets.
  • It's a real win for one HTTP request over many. On a page with a dozen tiny icons, trading a dozen requests for one slightly heavier HTML payload is often still a net gain, especially over HTTP/1.1 connections with limited parallelism.

Working With Base64 Without the Footguns

Between the standard/URL-safe alphabet split, the UTF-8 encoding step JavaScript doesn't handle for you, and the padding rules that differ by variant, doing base64 encode and base64 decode by hand invites exactly the kind of subtle bug this article covers. GenKitLab's Base64 Encoder/Decoder handles both standard and base64url alphabets, encodes and decodes full Unicode text correctly (no btoa() corruption), and converts a file directly to a base64 data URI for images. It runs entirely client-side — nothing you paste or upload ever leaves your browser.

If what you're actually decoding is a JWT, decode it with the JWT Decoder instead — it splits the token into its three base64url segments and decodes each one automatically, rather than requiring you to isolate and decode them by hand.

Frequently asked questions

Is base64 encryption?

No. Base64 is an encoding, not encryption — it has no key and provides no confidentiality. It's a reversible, publicly-documented mapping from bytes to text; anyone can decode a base64 string instantly with no secret required.

Why does base64 make data larger?

Because it re-expresses every 3 raw bytes as 4 text characters, each restricted to a 64-character alphabet. That's a fixed ~33% size increase with no exceptions — base64 never compresses data, it only makes binary safe to carry as text.

What's the difference between base64 and base64url?

Standard base64 uses + and / in its alphabet, both of which have special meaning inside a URL. Base64url replaces them with - and _, and typically drops the = padding, so the result is safe to use directly in a URL, filename, or HTTP header without escaping. JWTs use base64url for exactly this reason.

Why does btoa() break on some text but not other text?

btoa() only encodes correctly when every character's UTF-16 code unit is 255 or below — plain ASCII, effectively. For characters within that range but still multi-byte in UTF-8 (like é), it runs without error but produces the wrong bytes. For characters entirely outside that range (like most emoji), it throws an InvalidCharacterError instead.

How do I correctly base64 encode text with emoji or accented characters in JavaScript?

Convert the string to real UTF-8 bytes with TextEncoder before base64-encoding those bytes, and reverse the process with TextDecoder after decoding — don't pass the raw JavaScript string straight into btoa().

When does it make sense to convert a file to base64?

When embedding a small, frequently-referenced asset — an icon, a tiny image, a small font — directly into HTML, CSS, or JSON to avoid a separate HTTP request. It costs about 33% extra size and the browser can no longer cache that asset independently, so it's a poor fit for anything large or infrequently reused.

Last updated