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(), notfromCharCode(). These characters are aboveU+FFFF, sofromCharCode()can't create them in one call. -
[...text]instead oftext.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+24B6for โถ). 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.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.