Inline icons as data URIs with a Base64 converter in 2026
To convert an image to Base64, read its raw bytes, Base64-encode them, and prefix the result with
data:<mime-type>;base64,so a browser can use it as a data URI insrcorurl(). Expect the output to be about 33% larger than the file. Inline small icons (under ~4 KB) and keep everything else as a normal cached file.
The image to Base64 converter I link to below is one I built. I tried six online encoders first, and every one either uploaded my file to a server or handed me a bare string, so I still had to write the data URI prefix and the CSS wrapper by hand. Mine is free, runs client-side, needs no signup and never uploads the image. If you have a better one, tell me.
Why I stopped hand-rolling base64 strings
Three weeks ago I was cleaning up an internal dashboard that loaded 14 tiny status icons as separate requests. Each one was under 2 KB. Inlining them into the stylesheet as data URIs looked like an easy afternoon. I wrote a small shell loop around base64, piped the output into a CSS file, checked it on my Mac, and pushed.
CI built it on Ubuntu. Half the icons vanished.
It took me 47 minutes to figure out why. GNU coreutils base64 wraps its output at 76 characters by default. The macOS version doesn't. So on Linux my strings had newlines baked into them, and an unescaped newline inside a quoted CSS string makes the browser drop the whole declaration. No console error. No warning in the build. Just blank squares where icons used to be. The fix is base64 -w 0, a flag I now know by heart and resent slightly.
That wasn't my first encoding mess, either. Over the years I've shipped image/jpg instead of image/jpeg (browsers mostly forgive it, validators don't), base64-encoded SVGs that would have been smaller as URL-encoded text, and once reviewed a PR where someone inlined a 380 KB hero photo straight into a React component. The bundle roughly doubled. Nobody noticed until the Lighthouse score dropped.
The encoding itself is trivial. Everything around it is where people trip: the MIME type, the line wrapping, the wrapper syntax for CSS versus HTML, and the question of whether you should inline the thing at all.
What actually happens when you convert an image to base64
Base64 takes your file three bytes at a time. Those 24 bits get split into four 6-bit groups, and each group maps to one of 64 ASCII characters (A-Z, a-z, 0-9, +, /). If the file length isn't divisible by three, the last group gets padded with =. That's the whole algorithm, and it's why the output is always 4 * ceil(n / 3) characters long. For any decent-sized file, that works out to a 33.3% size increase.
A data URI wraps that string in a format defined back in RFC 2397: data:[<mediatype>][;base64],<data>. The media type matters. Get it wrong and the browser may refuse to render the image, or render it as something else entirely.
Here's a minimal Node script that does the job properly. It picks the MIME type from the extension, encodes without line breaks (Node's Buffer never wraps), and prints both the CSS and HTML forms. Save it as to-data-uri.mjs and run it with Node 18 or newer.
// to-data-uri.mjs
import { readFile } from "node:fs/promises";
import { extname } from "node:path";
const MIME = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".svg": "image/svg+xml",
};
const file = process.argv[2];
const mime = file && MIME[extname(file).toLowerCase()];
if (!mime) {
console.error(`Usage: node to-data-uri.mjs <image.png|jpg|gif|webp|svg>`);
process.exit(1);
}
const bytes = await readFile(file);
const b64 = bytes.toString("base64");
const dataUri = `data:${mime};base64,${b64}`;
const growth = ((b64.length / bytes.length - 1) * 100).toFixed(1);
console.log(`raw: ${bytes.length} bytes`);
console.log(`base64: ${b64.length} chars (+${growth}%)`);
console.log(`css: background-image: url("${dataUri.slice(0, 48)}...");`);
console.log(`html: <img src="${dataUri.slice(0, 48)}..." alt="">`);
// $ node to-data-uri.mjs icon-check.png
// raw: 1247 bytes
// base64: 1664 chars (+33.4%)
// css: background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB...");
// html: <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB..." alt="">
That iVBORw0KGgo prefix is the PNG file signature after encoding. Once you've seen it a few times you can spot a PNG data URI from across the room, which is a deeply useless party trick.
The script truncates output so it fits in a terminal. In real use you'd write the full dataUri to a file or the clipboard.
If you don't want to keep a script around, the image to Base64 converter does the same thing in the browser. You drop in a PNG, JPG, GIF, WebP or SVG and get the raw string, the full data URI, a CSS background-image rule and an HTML <img> tag, ready to copy. I built it mostly because I kept rewriting the snippet above on different machines and forgetting which flags each base64 binary wanted.
How it compares to the CLI, build tools and upload sites
There are plenty of ways to get the same string. Which one fits depends on whether this is a one-off or part of a build. Here's how I'd rank them after using all of them on real projects:
| Option | Where it runs | Output you get | Main gotcha | Good for |
|---|---|---|---|---|
base64 CLI |
Your terminal | Raw string only | GNU wraps at 76 chars unless you pass -w 0; macOS doesn't wrap |
Quick checks when you remember the flags |
Node Buffer script |
Your terminal or CI | Whatever you code | You maintain the MIME map yourself | Scripted pipelines, batch jobs |
Vite assetsInlineLimit
|
Build step | Inlined automatically in the bundle | Default threshold is 4096 bytes, easy to forget it exists | Apps already on Vite |
| Upload-based web converters | Someone else's server | Usually raw string, sometimes data URI | Your file leaves your machine; often ad-heavy | Nothing I'd recommend for private assets |
| aidevhub image-to-base64 | Your browser | Raw, data URI, CSS and HTML | Manual copy-paste, not wired into a build | One-off icons, emails, prototypes, docs |
My honest take: if you're on Vite or a similar bundler, let the build tool handle inlining and don't paste strings by hand at all. The build knows the file, picks the MIME type, and redoes the work when the image changes. Hand-pasted base64 goes stale the moment a designer updates the icon, and nobody can review a 1,664-character string in a diff.
The browser tool (mine or anyone's) earns its place when there's no build step. Think a standalone HTML file, a README badge, a CodePen, a bookmarklet, or a single icon in a config file. That's most of what I use it for now.
When you should not inline an image
This is the part most "convert image to base64" tutorials skip, and it's the part that actually matters.
Anything bigger than a few KB. The 33% overhead is the obvious cost. The less obvious one is where the bytes end up. Base64 inside a stylesheet makes that stylesheet bigger, and CSS is render-blocking. A 60 KB inlined background delays first paint for every page that loads the file, even pages that never show the image. Gzip claws back some of the overhead on the wire, but the browser still has to download, decompress and parse all of it before rendering. I use 4 KB as my cutoff because that's Vite's default and I've never had a reason to argue with it.
Images reused across pages. A separate logo.png gets cached once. A logo inlined into five HTML templates gets downloaded five times. On HTTP/2 and HTTP/3, extra requests are cheap enough that the old "save a round trip" argument mostly falls apart. I'll admit I haven't measured this on every CDN setup, so maybe there's a config where inlining still wins for medium-sized files. I haven't found one yet.
HTML email. This is where I got burned hardest. Last year I built a transactional email template with an inlined logo. It looked perfect in Apple Mail. Gmail's web client wouldn't display it, and Outlook desktop was inconsistent. For email, host the image or use CID attachments. Data URIs are a trap there.
Strict Content Security Policy. If your CSP has img-src 'self' without data:, inlined images just won't load. Adding data: to the policy is usually fine for images, but check with whoever owns your security headers before you widen it.
SVGs, most of the time. An SVG is already text. Base64-encoding it adds the 33% and makes it compress worse, because gzip loves repeated XML tags and hates base64 noise. URL-encoding the SVG (escape #, <, > and quotes, then use data:image/svg+xml,...) is usually smaller and stays readable. I still offer SVG input in the tool because some contexts only accept base64 (a few email builders and older APIs), but it's rarely my first choice.
Large hero images and LCP elements. Inlining the biggest image on the page feels like it should make it appear faster. In my experience it doesn't. You lose responsive srcset, you lose lazy loading control, and the whole HTML document gets heavier. Use a normal <img> with a preload hint instead.
FAQ
Q: Why is the Base64 output bigger than my original image?
A: Base64 turns every 3 bytes into 4 characters, so output is about 33% larger. A 1,247-byte PNG becomes 1,664 characters. Gzip reduces the gap on the wire, but you never get back to the original size.
Q: Does converting an image to Base64 hide or protect it?
A: No. Base64 is plain encoding, and anyone can decode it in one line (atob() in a browser, base64 -d in a terminal). Don't treat it as obfuscation for anything sensitive.
Q: Should I use image/jpg or image/jpeg in a data URI?
A: Use image/jpeg. It's the registered MIME type. Most browsers tolerate image/jpg, but some validators and email clients don't, and it costs you nothing to get it right.
Q: Is there a size limit on data URIs?
A: Modern browsers accept very large ones in practice, so you'll hit performance problems long before you hit a hard limit. If you're asking this question, the image is probably too big to inline.
Written with AI assistance and human review. Try the tool at aidevhub.io/image-to-base64.
Top comments (0)