Social media has no font controls. Instagram offers no bold button. Discord has no cursive setting. TikTok gives you exactly one typeface. And yet styled text is everywhere — cursive bios, gothic server names, bold display names. Every one of them runs on the same underlying mechanism, and it is not what most people think.
The mechanism in one sentence
A "fancy font generator" does not apply fonts. It substitutes each input character with a visually similar character from a different Unicode block.
Your regular A is U+0041. Mathematical Bold Capital A is U+1D400. They look related but they are entirely different code points. Copy 𝐀 into any plain-text field and it survives — because it is not formatting, it is a character.
The mapping in code
The entire engine of every fancy text tool is a lookup table and a map operation:
const BOLD = {
A: '\u{1D400}', B: '\u{1D401}', C: '\u{1D402}',
D: '\u{1D403}', E: '\u{1D404}', F: '\u{1D405}',
// ... full A-Z
a: '\u{1D41A}', b: '\u{1D41B}', c: '\u{1D41C}',
// ... full a-z
};
function convert(text, charMap) {
return [...text]
.map(char => charMap[char] || char)
.join('');
}
convert('Hello', BOLD); // '𝐇𝐞𝐥𝐥𝐨'
Three implementation details that matter:
Spread syntax over split(''). The [...text] spread iterates by code point, not by UTF-16 code unit. Input containing emoji or supplementary-plane characters will break with split('') because it splits surrogate pairs. Every fancy text tool that uses split('') has this bug — most just never notice because typical input is ASCII.
Fallthrough for unmapped characters. The || char keeps spaces, digits, and punctuation intact. Without it, spaces become undefined and the output is garbage.
Curly-brace escapes for supplementary plane. \u{1D400} requires the braces because the code point exceeds U+FFFF. Writing \u1D400 (no braces) compiles as \u1D40 followed by literal 0 — a silent bug that produces wrong output for exactly one character position.
The alphabets available
Unicode provides these mathematical alphabet variants, each in its own block:
| Style | Lowercase example | Block range |
|---|---|---|
| Bold Serif | 𝐚𝐛𝐜 | U+1D400–1D433 |
| Italic | 𝑎𝑏𝑐 | U+1D434–1D467 |
| Bold Italic | 𝒂𝒃𝒄 | U+1D468–1D49B |
| Script (cursive) | 𝒶𝒷𝒸 | U+1D49C–1D4CF |
| Bold Script | 𝓪𝓫𝓬 | U+1D4D0–1D503 |
| Fraktur (gothic) | 𝔞𝔟𝔠 | U+1D504–1D537 |
| Bold Fraktur | 𝖆𝖇𝖈 | U+1D56C–1D59F |
| Double-struck | 𝕒𝕓𝕔 | U+1D538–1D56B |
| Monospace | 𝚊𝚋𝚌 | U+1D670–1D6A3 |
| Sans-serif | 𝖺𝖻𝖼 | U+1D5A0–1D5D3 |
| Sans Bold | 𝗮𝗯𝗰 | U+1D5D4–1D607 |
| Sans Italic | 𝘢𝘣𝘤 | U+1D608–1D63B |
| Sans Bold Italic | 𝙖𝙗𝙘 | U+1D63C–1D66F |
That is 13 complete Latin alphabets — uppercase and lowercase — sitting in Unicode primarily because mathematicians needed them for notation.
The gaps that break naive implementations
This is where most implementations fail. Several alphabets have "holes" — code points that were never assigned because the character already existed elsewhere in Unicode under a different name.
// Mathematical Italic is missing lowercase h
// U+1D455 was reserved but never assigned
// The correct character is U+210E (Planck constant)
const ITALIC = {
// ...
h: '\u210E', // NOT '\u{1D455}'
// ...
};
The pattern repeats across multiple alphabets:
-
Script:
e→U+212F,g→U+210A,o→U+2134 -
Fraktur:
C→U+212D,H→U+210C,I→U+2111,R→U+211C,Z→U+2128 -
Double-struck:
C→U+2102,H→U+210D,N→U+2115,P→U+2119,Q→U+211A,R→U+211D,Z→U+2124
These characters live in the Letterlike Symbols block (U+2100–U+214F), not in the Mathematical Alphanumeric Symbols block. A generator that naively computes character offsets without handling these exceptions will output undefined or a wrong character for specific letters — a bug that is invisible unless someone types a word containing one of the affected letters.
Combining marks — the second mechanism
Beyond alphabet substitution, Unicode has combining characters that stack onto the preceding base character:
function strikethrough(text) {
return [...text]
.map(char => char === ' ' ? ' ' : char + '\u0336')
.join('');
}
strikethrough('deleted'); // 'd̶e̶l̶e̶t̶e̶d̶'
U+0336 (Combining Long Stroke Overlay) has zero advance width — it renders directly on top of the character before it. The same principle gives you:
- Underline:
U+0332 - Overline:
U+0305 - Double underline:
U+0333 - Dot above:
U+0307 - Zalgo (stacked chaos): multiple combining marks per character
The space check (char === ' ') matters because combining marks on whitespace render as floating diacritics, which looks broken rather than styled.
Cross-device rendering: what actually works
Every styled character needs a font on the receiving device that contains it. Modern iOS, Android, and desktop operating systems ship fonts covering the full Mathematical Alphanumeric Symbols block. The practical coverage:
| Style | iOS | Android | Windows | Coverage |
|---|---|---|---|---|
| Bold/Italic/Bold Italic | ✅ | ✅ | ✅ | Universal |
| Script (both weights) | ✅ | ✅ | ✅ | Universal |
| Fraktur (both weights) | ✅ | ✅ | ✅ | Universal |
| Double-struck | ✅ | ✅ | ✅ | Universal |
| Monospace | ✅ | ✅ | ✅ | Universal |
| Sans variants | ✅ | ✅ | ✅ | Universal |
| Rare combining stacks | ⚠️ | ⚠️ | ⚠️ | Varies |
The mainstream styles work everywhere. Exotic combining-character stacks (heavy zalgo, multi-diacritic effects) render inconsistently on budget Android devices that ship with minimal font coverage.
The accessibility cost
Screen readers do not recognize mathematical alphabet characters as their plain-text equivalents. NVDA encountering 𝗛𝗲𝗹𝗹𝗼 may read it as "mathematical sans-serif bold capital H, mathematical sans-serif bold small e..." — one Unicode name per character.
For decorative display text (usernames, bio taglines), this is an accepted trade-off. For anything informational — instructions, descriptions, critical content — real formatting (CSS font-weight, markdown **bold**) is correct and styled Unicode is wrong.
Tools that surface this distinction rather than hiding it serve their users better than tools that pretend every context is appropriate for character substitution.
Search implications
Styled characters and plain characters are distinct code points. A search for "admin" will not match "𝗮𝗱𝗺𝗶𝗻" on any platform — they share zero characters in common. This has real consequences:
- Styled Discord channel names are unsearchable by their apparent name
- Styled hashtags reach nobody (they are different tags)
- Styled usernames cannot be @mentioned by typing the visible name
These are not bugs — they are the logical consequence of the styling being characters rather than formatting.
Building one yourself
A complete implementation needs three things:
- Correct character maps — including Letterlike Symbols block exceptions
- Combining mark functions — for strikethrough, underline, and decorative effects
- A normalization function — to strip existing styled characters back to ASCII before re-converting
The normalization matters for paste-in scenarios: if a user pastes already-styled text and tries to convert it to a different style, you need to reverse the first conversion before applying the second.
function normalize(text) {
return [...text].map(char => {
const cp = char.codePointAt(0);
// Check each mathematical block range
// and map back to basic Latin
if (cp >= 0x1D400 && cp <= 0x1D419)
return String.fromCharCode(65 + cp - 0x1D400); // Bold A-Z
if (cp >= 0x1D41A && cp <= 0x1D433)
return String.fromCharCode(97 + cp - 0x1D41A); // Bold a-z
// ... repeat for each block
return char;
}).join('');
}
The full implementation is longer but mechanical — each block follows the same offset pattern with its specific exceptions.
Why this matters beyond fun
Unicode mathematical alphabets were added to the standard for academic typesetting. Their current primary use — making Instagram bios look fancy — was entirely unintended. But it solves a real problem: platforms that accept text but offer no formatting. Until Instagram adds a bold button or Discord lets you choose a bio font, character substitution remains the only way to style text in those contexts.
The engineering is trivial. The edge cases are not. Getting the Letterlike Symbols exceptions right, handling surrogate pairs in input, managing combining mark stacking, and testing cross-device rendering is where amateur implementations diverge from correct ones.
If you want to see how different organizational approaches to the same underlying mechanism feel from a user perspective, this tool groups 200+ variations by visual category while handling all the exception cases covered above.
Top comments (0)