
I typed Größe in this sitting. TextEncoder produced the bytes 71, 114, 195, 182, 195, 159, 101. This tab encoded those bytes as R3LDtsOfZQ==.
btoa on the same string did not match that. In this environment it returned a different latin-1 reading. The letters did not throw. They came back wrong.
That is the usual miss. A German noun, a name, a city goes into btoa. The round trip looks like Base64. It is not the UTF-8 of what you typed.
The new page is Base64 Encoder and Decoder. You paste text, drop a file, or paste a data URI. The tab picks encode or decode. It uses UTF-8. Padding errors are named. The bytes stay in the tab. Use the sample if you want the Größe path without typing the ß. I put that word on the button because it is the bug.
The bytes of Größe
Größe has an ß. UTF-8 writes that as two bytes, 195 and 159. The rest of the word is ASCII. I printed the array in this sitting: 71, 114, 195, 182, 195, 159, 101.
RFC 4648 of those bytes is R3LDtsOfZQ==. Decode that string and you get Größe back, if the decoder uses TextDecoder.
I did the round trip in this sitting. Encode Größe. Copy R3LDtsOfZQ==. Paste it back. The result box said Größe. Same letters, same ß. That is the check. If the second paste prints something else, the decoder is not UTF-8.
A page that feeds the JavaScript string to btoa never sees those bytes. btoa wants latin-1. ß is not latin-1 in the way people mean when they say UTF-8. Some engines throw. Some produce a different string and stay quiet.
I ran both in this sitting. This tab: R3LDtsOfZQ==. btoa: not that. The test accepts a throw or a mismatch. Either way, btoa is not the UTF-8 path.
btoa is latin-1
The function is old. It maps each 16-bit code unit to a byte, as long as the unit is 0 to 255. A character above that throws in a strict engine. ß is U+00DF, which is 223, so some engines accept it and write one byte. UTF-8 writes two.
That one-byte reading is why a German word survives the call and still comes back garbage. The page did not crash. The output looks official. It will not round trip through a UTF-8 decoder.
This tab never calls btoa. Encode walks a Uint8Array from TextEncoder. Decode walks a lookup table, then TextDecoder with fatal set. A bad UTF-8 sequence is named. It is not shown as a replacement character.
I did not pull in a Base64 library. The lookup tables are in this repo. Encode groups three bytes into four characters. Decode rejects a length that is 1 mod 4, extra padding in the middle, and a character outside the alphabet. Those errors have names on the page. A silent empty box would have been easier, and wrong.
If you already have latin-1 bytes, say so by pasting the Base64 of those bytes. The tab will decode the bytes. It will not pretend they were UTF-8 if they were not.
Encode or decode, said out loud
Four letters like aaaa could be text or Base64. The page waits. You pick Encode or Decode. It will not print a huge result until you do.
Padding is a marker. R3LDtsOfZQ== decodes. Characters outside the alphabet encode. Größe has ß, so it encodes. A data URI that contains ;base64, decodes. A data URI without that marker is percent-encoded. The tab names that and will not pretend the payload is Base64. Encoding the whole string is the usual next step if you pasted it by mistake.
You can override. Auto is the default. Force Encode on a padded string if you really want to encode the equals signs as text. Force Decode when a short alphabet string is actually a payload.
This looks like Base64 (padded).
That sentence is the product. A result without it is a guess you cannot audit.
Four letters wait
aaaa is four characters from the alphabet and has no padding. It could be the word aaaa. It could be three bytes.
This tab does not pick. It prints the reason and two buttons, Use encode and Use decode. I typed aaaa in this sitting. No result box appeared until I clicked.
Guess in silence and you get a decoded blob nobody meant, or you encode a payload that was already Base64. Waiting is slower for one second. It is cheaper than a wrong file.
A string of sixteen or more alphabet characters without padding is treated as decode. Short ones wait. Padding decides sooner.
Padding that is wrong
RFC 4648 writes these short strings. I encoded each one in this sitting.
| Input | This tab |
|---|---|
| empty | empty |
| f | Zg== |
| fo | Zm8= |
| foo | Zm9v |
| foobar | Zm9vYmFy |
| Größe | R3LDtsOfZQ== |
| one zero byte | AA== |
Missing padding on Zg is valid here. The tab adds two equals signs and says so. Zg== is already padded, so it does not get two more. Adding padding twice would have turned a valid string into Zg====, which is garbage. The decoder only fills when no equals sign is present.
Zg=== is an error. Extra padding after a complete group is not a byte. A single leftover character cannot be a group of four. An invalid character like AAA! is named as an invalid character, not as a mysterious fail.
Whitespace is stripped. A pasted block with line breaks still decodes. MIME wrap 76 is a newline every 76 characters on encode. Decode does not need you to unwrap first.
A zero byte is still a byte
AA== is one zero. I decoded it in this sitting. The result is not an empty text box. A NUL is bytes. The page prints a hex dump and offers a download.
A form that stops at the first empty character will eat it. Copy as text will also eat it in some editors. Download decoded.bin if the bytes matter.
Looks-binary is a NUL or a sequence TextDecoder refuses. JSON that happens to contain a NUL is bytes here. Take the text to JSON Formatter and Validator only after it is actually UTF-8 text.
Hex dump on this page prints the first 32 bytes, two digits per byte, spaced. I printed 00 ff 10 from the bytes 0, 255, 16 in this sitting. The rest of a long binary payload is in the download. The textarea is the wrong place for it.
Files and a data URI
Drop a PNG and the tab sniffs 89 50 4E 47, then writes data:image/png;base64,... when the data URI switch is on. The image renders here so you can see it before you copy the string.
A file over 20,000,000 bytes is refused. That cap is 20 MB SI, counted in this sitting. Inlining a 2 MB photo as a data URI is how a stylesheet becomes a download. Shrink it first on Compress Image to Exact KB.
The first 8,192 characters of a long result show on the page. The rest is a download. Putting 20 MB into a textarea locks the tab.
A file over about 256 KB encodes in a worker, with a progress number and a cancel. Cancel is checked per chunk. I confirmed the progressive encoder matches the one-shot encoder on the same bytes. A cancel flag that is read once at the top of the function is not cancel. This one is read between chunks of 12,288 bytes (3 times 4,096).
Pick Decode, then choose a file of Base64 text, and the tab treats the file as UTF-8 text and decodes it. A binary file in Decode is refused, because it is not text of an alphabet. Auto on a dropped file still encodes. That is the usual job: a PNG in, a data URI out.
URL alphabet and wrap 76
JWT segments and some filenames use Base64URL: hyphen and underscore instead of plus and slash, padding often dropped. I encoded the bytes 251 and 255 with the URL alphabet and no padding in this sitting. The result is -_8. No plus, no slash, no equals.
Pick URL. Auto will pick URL when the paste uses hyphen or underscore and stays inside that alphabet.
MIME likes a newline every 76 characters. That is wrap 76. Both knobs stay in this browser. They do not go out with the paste.
Encoding is not encryption
Anyone who has this tab can reverse what you just did. Base64 is an alphabet for bytes, the same job as hex. A password encoded here is still the password. A private key encoded here is still the key.
The conversion runs in this tab. That is not a lock. If you need a lock, this page is not it.
People paste certificates and JSON that happen to be Base64 because that is how those files travel. The page will decode them. It will not keep them. It will not encrypt them. The share link does not put the paste in the URL on purpose, because a paste can be a secret even when the encoding is not a lock.
I almost put the sample in the share URL the way Epoch puts a timestamp in ts=. A Unix time is a public instant. Größe is a word. The next paste might be a key. The preview line is the reminder: this link carries the knobs. It does not carry the paste.
Settings, a link, a preview
Direction, alphabet, wrap and the data URI switch live in this browser under softery.base64-encoder-decoder.settings. The paste is not stored. Clear settings puts Auto back. Clear paste empties the box and leaves the knobs.
Copy link puts those knobs in the URL. It does not put the text in the URL. You see the preview before it is copied: auto direction, auto alphabet, wrap, data URI or plain Base64. The sentence says it does not carry the paste.
A settings URL is useful when the card is the knobs. It is a bad idea for a private document. The preview is there so you can tell which one you are about to send.
Download on encode writes encoded.b64, or encoded.txt for a data URI. Download on decode writes decoded.txt when the bytes are UTF-8 text, and decoded.bin when they are not. Open the file. If it does not contain the bytes you saw on screen, the download failed.
What Mixpanel sees
Mixpanel and Google Analytics load with the rest of Softery.io. They see that someone opened the tool. They get the knob names, a size bucket, and whether a copy or a download happened. They do not get the paste.
The Mixpanel slug is base64-encoder-decoder. That is the URL slug. Setting names go out as encode, decode, Auto, standard, url, 76. Sizes go out as buckets. The paste does not. The decoded bytes do not.
If you came here because a privacy page promised zero third-party scripts, that is not this site. I will not write that sentence. After the page has loaded you can unplug the cable and still go from Größe to R3LDtsOfZQ==. The math is JavaScript here. The telemetry is the site, not the paste.
Nearby tools
Decoded JSON belongs on JSON Formatter and Validator. A photo you are about to inline belongs on Compress Image to Exact KB. Both stay in the tab.
Base64 Encoder and Decoder is the fortieth free tool on the hub. It runs after the JavaScript has loaded. Unplug the cable if you want. The bytes are still in the tab. I counted the hub cards in this sitting. This page is the fortieth.
- Open Base64 Encoder and Decoder.
- Hit Use the sample, or paste Größe.
- Read R3LDtsOfZQ==.
- Paste that string back. You should see Größe. Paste aaaa next, and wait for the buttons.
Last updated: September 18, 2026 | Reading time: 10 minutes
Written by Softery.io, after btoa returned the wrong letters for Größe.