Back to News & Insights
JavaScript August 25, 2026 · 6 min read

Building a Base64 Tool That Actually Handles UTF-8 (Without Losing Your Mind)

The Problem That Started It All I was working on a project that required sending...

Building a Base64 Tool That Actually Handles UTF-8 (Without Losing Your Mind)

I was working on a project that required sending configuration data between different services. The payload contained Chinese characters, emojis, and the occasional accented character from a French colleague's name in the comments. Classic stuff.

The first time I tested it, everything worked. The second time, I got back a string of é and ’ instead of actual text. You know the drill — mojibake. The kind of thing that makes you question every life decision that led you to this moment.

The root cause? I was using btoa() and atob() directly on strings containing non-ASCII characters. These functions only handle Latin-1 (ISO-8859-1) natively. Feed them a Chinese character and they'll happily mangle it into something that looks like it fell out of a corrupted file.

I could have opened any of the dozens of online Base64 converters. But I had specific requirements: I needed something that handles UTF-8 correctly (the mojibake problem above) I wanted it to work offline I didn't want to paste sensitive config data into some random website I was tired of tools that required clicking a "Convert" button when I just wanted live feedback

So I decided to build a small browser-based tool. Because apparently I enjoy reinventing wheels.

The TextEncoder converts the string to UTF-8 bytes, and String.fromCharCode(...) turns those bytes into a string that btoa() can handle. The decode path reverses this process.

One gotcha: for very large inputs, String.fromCharCode(...array) can blow the call stack. In production, you'd want a chunked approach. But for typical text inputs, this works fine.

The most interesting design decision was whether to convert on every keystroke or wait for user action.

Option A: Real-time conversion Pro: Instant feedback, feels magical Con: Can be janky with large inputs, potentially wasteful

Option B: Manual conversion button Pro: Predictable, performant Con: Extra click, feels dated

I ended up going with a hybrid: real-time by default, but with a manual override. Users can toggle it off if they're pasting a massive file. The implementation is simple — just listen to the input event and debounce:

The debounce is critical. Without it, every keystroke triggers a full conversion cycle, which causes noticeable lag on longer inputs.

Here's something I didn't think about until I started building: when you swap between encode and decode modes, what happens to the existing input?

If someone types "hello" in encode mode, then switches to decode mode, should "hello" be decoded as Base64? That would produce garbage. The sensible behavior is to clear the input when the mode changes.

But wait — there's a more interesting UX pattern. What if the user wants to decode something they just encoded? They'd have to copy the output, switch modes, and paste it back. That's annoying.

This is where the swap button comes in. Instead of manually copying and switching, one click moves the current output into the input field and flips the mode. It's a small touch, but it makes the tool feel much more fluid.

Decoding invalid Base64 is a surprisingly common failure mode. Users paste in text that looks like Base64 but isn't — maybe it has a space in it, or they copied a URL fragment by accident.

This works, but it's not very helpful. The better approach is to validate the input format first and give specific feedback:

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation