Dev.to WebDev ๐Ÿ›  Dev ๐Ÿ‘ 0 ๐Ÿ“– 8 min read

How Fancy Text Generators Actually Work (Itโ€™s Just Unicode)

You've seen it in Instagram bios, Twitter names and Discord servers: text like ๐๐จ๐ฅ๐, ๐“ข๐“ฌ๐“ป๐“ฒ๐“น๐“ฝ or ๐”พ๐• ๐•ฅ๐•™๐•š๐•” in apps that don't let you pick a font at all. So how does it work? Short answer: it isn't a font. Fancy text generat

You've seen it in Instagram bios, Twitter names and Discord servers: text like ๐๐จ๐ฅ๐, ๐“ข๐“ฌ๐“ป๐“ฒ๐“น๐“ฝ or ๐”พ๐• ๐•ฅ๐•™๐•š๐•” in apps that don't let you pick a font at all. So how does it work?

Short answer: it isn't a font. Fancy text generators swap each normal letter for a different Unicode character that happens to look styled. In this post I'll show you exactly which characters they use, the few lines of JavaScript that do the conversion, and the edge cases that broke my own generator when I built one.

It's Not a Font, It's a Different Character

A fancy text generator doesn't change how a letter looks. It replaces the letter with a completely separate character. A font is a visual skin applied to the same character, but ๐€ and A are two different characters with two different code points.

You can prove it in your browser console:

console.log("๐€" === "A");                       // false
console.log("๐€".codePointAt(0).toString(16));   // "1d400"

U+1D400 is officially named MATHEMATICAL BOLD CAPITAL A. Because it's a real character and not styling, it survives copy and paste. That's why it works in places with no formatting options: an Instagram bio, a username field, a plain <textarea>.

Where These Characters Come From: The Mathematical Alphanumeric Symbols Block

Most "fonts" in a fancy text generator come from one Unicode block called Mathematical Alphanumeric Symbols, which runs from U+1D400 to U+1D7FF. It was added in Unicode 3.1 (2001) for mathematicians, not for social media. In math notation, a bold x, an italic x and a script ๐“ can mean three different things, so each one needed its own character to keep that meaning in plain text.

The useful part for developers is how the block is laid out. Each style is a neat run of 26 capitals followed by 26 lowercase letters:

Style Example Capital A Small a
Bold ๐€๐š U+1D400 U+1D41A
Italic ๐ด๐‘Ž U+1D434 U+1D44E
Script ๐’œ๐’ถ U+1D49C U+1D4B6
Bold Script ๐“๐“ช U+1D4D0 U+1D4EA
Fraktur ๐”„๐”ž U+1D504 U+1D51E
Double-struck ๐”ธ๐•’ U+1D538 U+1D552
Sans-serif ๐– ๐–บ U+1D5A0 U+1D5BA
Monospace ๐™ฐ๐šŠ U+1D670 U+1D68A

The block also holds bold, double-struck, sans-serif and monospace digits. Bold ๐ŸŽ starts at U+1D7CE.

Because the letters sit in alphabetical order, converting text is just arithmetic.

The Character Offset Trick in JavaScript

To convert a letter, find its position in the alphabet (A = 0, B = 1 and so on) and add that number to the style's starting code point. That's the whole trick.

Here's the simplest version, a bold converter:

function toBold(text) {
  return [...text].map((ch) => {
    const code = ch.codePointAt(0);
    if (code >= 65 && code <= 90) return String.fromCodePoint(0x1d400 + (code - 65));  // A-Z
    if (code >= 97 && code <= 122) return String.fromCodePoint(0x1d41a + (code - 97)); // a-z
    return ch; // leave spaces, numbers and punctuation alone
  }).join("");
}

console.log(toBold("Hello, World!")); // ๐‡๐ž๐ฅ๐ฅ๐จ, ๐–๐จ๐ซ๐ฅ๐!

Two details matter here:

  • String.fromCodePoint(), not fromCharCode(). These characters are above U+FFFF, so fromCharCode() can't create them in one call.
  • [...text] instead of text.split(""). Spreading a string iterates by code point, so it won't cut a fancy character in half. More on that later.

To support every style, store the starting points in a map and reuse one function:

const STYLES = {
  bold:         { upper: 0x1d400, lower: 0x1d41a },
  italic:       { upper: 0x1d434, lower: 0x1d44e },
  script:       { upper: 0x1d49c, lower: 0x1d4b6 },
  boldScript:   { upper: 0x1d4d0, lower: 0x1d4ea },
  fraktur:      { upper: 0x1d504, lower: 0x1d51e },
  doubleStruck: { upper: 0x1d538, lower: 0x1d552 },
  sans:         { upper: 0x1d5a0, lower: 0x1d5ba },
  monospace:    { upper: 0x1d670, lower: 0x1d68a },
};

function stylize(text, style) {
  const { upper, lower } = STYLES[style];
  return [...text].map((ch) => {
    const code = ch.codePointAt(0);
    if (code >= 65 && code <= 90) return String.fromCodePoint(upper + code - 65);
    if (code >= 97 && code <= 122) return String.fromCodePoint(lower + code - 97);
    return ch;
  }).join("");
}

console.log(stylize("Hello", "boldScript")); // ๐“—๐“ฎ๐“ต๐“ต๐“ธ
console.log(stylize("Hello", "monospace"));  // ๐™ท๐šŽ๐š•๐š•๐š˜

This works perfectly for bold, bold script, sans-serif and monospace. Now try double-struck.

Why Some Letters Come Out Broken

Run stylize("Hello", "doubleStruck") and the H comes out as an empty box or a blank space. The code is fine. The character it calculated, U+1D53F, is simply reserved and empty in Unicode.

The reason is history. Before the math block existed, a few of these letters were already encoded in the older Letterlike Symbols block (U+2100 to U+214F). Mathematicians had been using โ„ for real numbers, โ„• for natural numbers and โ„Ž for Planck's constant for decades. Unicode has a rule against encoding the same character twice, so when the math block was added in 2001, those slots were left empty and the original characters stayed where they were.

That leaves 23 "holes" across four styles:

Style Missing letters (found in Letterlike Symbols instead)
Italic h โ†’ โ„Ž
Script B E F H I L M R e g o โ†’ โ„ฌ โ„ฐ โ„ฑ โ„‹ โ„ โ„’ โ„ณ โ„› โ„ฏ โ„Š โ„ด
Fraktur C H I R Z โ†’ โ„ญ โ„Œ โ„‘ โ„œ โ„จ
Double-struck C H N P Q R Z โ†’ โ„‚ โ„ โ„• โ„™ โ„š โ„ โ„ค

Bold, bold script, sans-serif and monospace have no holes, which is why they're the safest styles to generate.

The fix is an exception map that's checked before the offset math runs:

const EXCEPTIONS = {
  italic:       { h: "โ„Ž" },
  script:       { B: "โ„ฌ", E: "โ„ฐ", F: "โ„ฑ", H: "โ„‹", I: "โ„", L: "โ„’", M: "โ„ณ", R: "โ„›", e: "โ„ฏ", g: "โ„Š", o: "โ„ด" },
  fraktur:      { C: "โ„ญ", H: "โ„Œ", I: "โ„‘", R: "โ„œ", Z: "โ„จ" },
  doubleStruck: { C: "โ„‚", H: "โ„", N: "โ„•", P: "โ„™", Q: "โ„š", R: "โ„", Z: "โ„ค" },
};

function stylize(text, style) {
  const { upper, lower } = STYLES[style];
  const exceptions = EXCEPTIONS[style] || {};
  return [...text].map((ch) => {
    if (exceptions[ch]) return exceptions[ch];
    const code = ch.codePointAt(0);
    if (code >= 65 && code <= 90) return String.fromCodePoint(upper + code - 65);
    if (code >= 97 && code <= 122) return String.fromCodePoint(lower + code - 97);
    return ch;
  }).join("");
}

console.log(stylize("Bright Ideas", "script"));    // โ„ฌ๐“‡๐’พโ„Š๐’ฝ๐“‰ โ„๐’นโ„ฏ๐’ถ๐“ˆ
console.log(stylize("Zero Hour", "fraktur"));      // โ„จ๐”ข๐”ฏ๐”ฌ โ„Œ๐”ฌ๐”ฒ๐”ฏ
console.log(stylize("Chris", "doubleStruck"));     // โ„‚๐•™๐•ฃ๐•š๐•ค

If you're building a generator, test every style against the full alphabet, not just "Hello World". I only caught the italic h when a user's name came out with a gap in it.

Not Every Style Lives in the Math Block

Some popular styles don't come from the math block at all, so the offset trick doesn't work for them. They need a lookup map built from characters scattered across other blocks.

  • Small caps (๊œฑแดแด€สŸสŸ แด„แด€แด˜๊œฑ) are borrowed from phonetic alphabets like the IPA. There's no complete set, so some letters are substitutes. For example, there's no small capital X, so most generators just use a normal x.
  • Bubble letters (โ’ทโ“คโ“‘โ“‘โ“›โ“”) come from the Enclosed Alphanumerics block (U+24B6 for โ’ถ). These happen to be in order, so the offset trick works here too.
  • Upside-down text uses lookalike characters such as ษ and ส‡, then reverses the string.

Here's small caps as a lookup map:

const SMALL_CAPS = {
  a: "แด€", b: "ส™", c: "แด„", d: "แด…", e: "แด‡", f: "๊œฐ", g: "ษข", h: "สœ", i: "ษช",
  j: "แดŠ", k: "แด‹", l: "สŸ", m: "แด", n: "ษด", o: "แด", p: "แด˜", q: "วซ", r: "ส€",
  s: "๊œฑ", t: "แด›", u: "แดœ", v: "แด ", w: "แดก", x: "x", y: "ส", z: "แดข",
};

const toSmallCaps = (text) =>
  [...text.toLowerCase()].map((ch) => SMALL_CAPS[ch] ?? ch).join("");

console.log(toSmallCaps("Small Caps")); // ๊œฑแดแด€สŸสŸ แด„แด€แด˜๊œฑ

What Developers Should Watch Out For

Fancy text looks harmless, but it can break string handling, search and accessibility in your own apps. Here are the four problems you're most likely to hit.

1. String length is wrong

Each math character is stored as two UTF-16 code units (a surrogate pair), so .length doubles:

const fancy = "๐‡๐ž๐ฅ๐ฅ๐จ";
console.log(fancy.length);       // 10, not 5
console.log([...fancy].length);  // 5
console.log(fancy.slice(0, 3));  // "๐‡" plus half of "๐ž", a broken character

This matters for character limits. If your bio field allows 150 characters and checks .length, a user typing fancy text gets roughly half that. For counts that match what people see, including emoji with skin tones, use Intl.Segmenter:

const text = "๐‡๐ข ๐Ÿ‘‹๐Ÿฝ";
const graphemes = [...new Intl.Segmenter("en", { granularity: "grapheme" }).segment(text)];
console.log(text.length);       // 9
console.log(graphemes.length);  // 4

2. Search and usernames don't match

To a computer, "๐€๐ฅ๐ข" and "Ali" are different strings, so a search for one won't find the other. It's also a common trick for creating lookalike usernames. Unicode normalization fixes most of it:

console.log("๐€๐ฅ๐ข".normalize("NFKC"));   // "Ali"
console.log("โ„๐•’๐•ž๐•ซ๐•’".normalize("NFKC")); // "Hamza"
console.log("โ’ถโ“›โ“˜".normalize("NFKC"));   // "Ali"
console.log("แด€สŸษช".normalize("NFKC"));   // "แด€สŸษช", unchanged

NFKC maps math letters and bubble letters back to plain ones, but not small caps, because those are real phonetic letters in Unicode's eyes. Normalize before you index, compare or check for duplicate usernames, and handle small caps separately if they matter to you.

3. Screen readers may not read it

Since these are math symbols, assistive technology doesn't always treat them as words. Depending on the screen reader, styled text may be read letter by letter, announced with long character names, or skipped. If you add fancy text to your own UI, keep anything important (names, buttons, links) in plain text, or provide an aria-label with the plain version.

4. Some devices show boxes

Every character needs a glyph in some installed font. Modern Android, iOS, Windows and macOS cover the math block well, but older devices and some embedded browsers may show empty boxes (often called "tofu"). Bold, italic and bubble letters are the safest. Rarer phonetic characters are the most likely to break.

I Built FontFizz Using This Exact Idea

Everything above is what powers FontFizz, a free fancy text generator I built. Under the hood it's the same offset trick, an exception map for every style with holes, and lookup tables for styles like small caps. On top of that it adds a live preview of every style as you type and one-click copy.

The biggest lesson from building it was the one in the "broken letters" section: the core conversion takes ten minutes, but testing every style against the full alphabet, digits and real names is where the real work is.

Wrapping Up

Fancy text isn't a font. It's a character swap, mostly into a Unicode block designed for math notation, with 23 historical gaps you need to patch by hand. Once you know that, the strange .length values, failed searches and screen reader issues all make sense.

What's the weirdest Unicode bug you've run into? Let me know in the comments.

๐Ÿ“ฐ Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.