DEV Community

chuanbuilds
chuanbuilds

Posted on

I stopped trusting "free" Instagram font generators with my captions — so I built a browser-only one

Every time I want a slightly-less-boring Instagram bio, I fall into the same trap. I type my caption into some "free font generator," and somewhere between the input box and the copy button I realize I just pasted an unposted caption into a third-party site I've never heard of. Most of these tools upload your text to render it, which means my half-finished caption is now sitting on someone else's server.

So I built the one I actually wanted: an Instagram font generator that runs 100% in the browser. Type, copy, paste. Nothing leaves the tab.

Here's the part that's actually interesting if you write code: these "fonts" aren't fonts at all.

It's all Unicode

Instagram doesn't let you load custom fonts in a bio or caption. What these generators do is swap each letter for a look-alike character from the Mathematical Alphanumeric Symbols block (U+1D400 and up). "Bold" Latin isn't a bold typeface — it's the codepoint U+1D400 for 𝐀, U+1D41A for 𝐚, and so on.

So a generator is really just a small map: a → 𝐚, b → 𝐛 ... for each style. The whole thing fits in a few hundred bytes.

The core is a code-point offset

Each style is an offset into that block. Upper-case A–Z, lower-case a–z, and digits 0–9 each have their own base. A minimal version looks like this:

function styleText(input, base) {
  let out = '';
  for (const ch of input) {            // for...of walks code points, not UTF-16 units
    const code = ch.codePointAt(0);
    if (code >= 0x41 && code <= 0x5A) {        // A–Z
      out += String.fromCodePoint(base.upper + (code - 0x41));
    } else if (code >= 0x61 && code <= 0x7A) { // a–z
      out += String.fromCodePoint(base.lower + (code - 0x61));
    } else if (code >= 0x30 && code <= 0x39) { // 0–9
      out += String.fromCodePoint(base.digit + (code - 0x30));
    } else {
      out += ch;                          // leave spaces, punctuation, emoji untouched
    }
  }
  return out;
}

// Bold starts at U+1D400 (A), U+1D41A (a), U+1D7CE (0)
styleText("Hello 2026", { upper: 0x1D400, lower: 0x1D41A, digit: 0x1D7CE });
// => "𝐇𝐞𝐥𝐥𝐨 𝟐𝟎𝟐𝟔"
Enter fullscreen mode Exit fullscreen mode

This offset trick only works for the contiguous styles (bold, italic, sans, mono, Fraktur…). Styles like bubble ⓐ or squared ② live in the Enclosed Alphanumerics block and aren't contiguous, so for those I just store a literal lookup table instead of computing an offset.

The for...of loop matters. If you index the string with input[i] you'll split emoji and astral characters into surrogate pairs and mangle them. codePointAt(0) plus iterating with for...of keeps emoji intact.

The lowercase h that broke my first build

This is the bug that cost me an hour. The italic style isn't perfectly contiguous: the code point you'd expect for italic lowercase hU+1D44E + 7 = U+1D455 — is unassigned. Unicode already had ℎ (U+210E, the Planck constant symbol) as a slanted h, so it reused that instead of allocating a new slot. My offset math produced a blank/tofu box for every "h".

The fix is a one-line exception in the map. Same story for a few script/cursive capitals (ℋ, ℐ, ℛ, ℨ) — they live outside the main block too. If you build your own, hard-code those handful of exceptions or your output will have mysterious boxes.

Copy without a library

Copying is just the Clipboard API:

await navigator.clipboard.writeText(styled);
Enter fullscreen mode Exit fullscreen mode

I add a document.execCommand('copy') fallback for older mobile browsers that don't support it yet, but on anything current the async API is all you need.

Why some phones show boxes

The stylized characters are real Unicode, but not every device ships a font that covers the Mathematical Alphanumeric Symbols block. That's why a style can look perfect on your laptop and turn into □□□ on a friend's older phone. The practical rule: test the exact style on the phone you'll post from, and if in doubt use bold or italic — those have the widest support. Also, don't style a whole paragraph; one word reads as intentional, a wall of 𝐛𝐨𝐥𝐝 reads as spam.

Why browser-only

The tool joins 100-plus other utilities in the UntrackedTools collection, and they're all client-side for the same reason: a caption, a draft bio, a config snippet — none of it should have to leave your machine to be transformed. No account, no upload, no analytics. Open the tab, style your text, close it.

If you post to Instagram (or TikTok/X — the characters paste anywhere Unicode goes), give the Instagram font generator a try. It's the one I use before every post now.

Top comments (0)