Every social media bio in a fancy font, every Discord username in gothic letters, every "fancy text generator" tool â they all run on the same twenty-line JavaScript trick. No libraries, no fonts, no rendering engine. Just a character map and a String.prototype.split call.
I want to show you exactly how it works, because once you see the pattern you can build one in about ten minutes.
The core insight
Unicode reserves entire alphabets for mathematical notation. Bold serif A through Z. Italic. Monospace. Fraktur. Double-struck. Script. These were added for math papers but they render on every modern device because they are part of the Unicode standard, not a font file.
Regular capital A is code point U+0041. Mathematical Bold Capital A is U+1D400. Different characters, same visual meaning. Copy ð and paste it into a form that only accepts plain text â it goes through, because it is plain text, just a different code point.
A "font style generator" is not styling anything. It is swapping every character in your input for the visually equivalent character in a mathematical alphabet.
The mapping
Here is the entire lookup table for bold serif:
const BOLD_SERIF = {
a: '\u{1D41A}', b: '\u{1D41B}', c: '\u{1D41C}', d: '\u{1D41D}',
e: '\u{1D41E}', f: '\u{1D41F}', g: '\u{1D420}', h: '\u{1D421}',
i: '\u{1D422}', j: '\u{1D423}', k: '\u{1D424}', l: '\u{1D425}',
m: '\u{1D426}', n: '\u{1D427}', o: '\u{1D428}', p: '\u{1D429}',
q: '\u{1D42A}', r: '\u{1D42B}', s: '\u{1D42C}', t: '\u{1D42D}',
u: '\u{1D42E}', v: '\u{1D42F}', w: '\u{1D430}', x: '\u{1D431}',
y: '\u{1D432}', z: '\u{1D433}',
A: '\u{1D400}', B: '\u{1D401}', C: '\u{1D402}', D: '\u{1D403}',
E: '\u{1D404}', F: '\u{1D405}', G: '\u{1D406}', H: '\u{1D407}',
I: '\u{1D408}', J: '\u{1D409}', K: '\u{1D40A}', L: '\u{1D40B}',
M: '\u{1D40C}', N: '\u{1D40D}', O: '\u{1D40E}', P: '\u{1D40F}',
Q: '\u{1D410}', R: '\u{1D411}', S: '\u{1D412}', T: '\u{1D413}',
U: '\u{1D414}', V: '\u{1D415}', W: '\u{1D416}', X: '\u{1D417}',
Y: '\u{1D418}', Z: '\u{1D419}'
};
The converter
function convert(text, map) {
return [...text].map(ch => map[ch] || ch).join('');
}
convert('Hello World', BOLD_SERIF);
// 'ðððĨðĨðĻ ððĻðŦðĨð'
Three things worth noticing:
[...text] instead of text.split('') â the spread operator splits by Unicode code points rather than UTF-16 code units. If the input already contains emoji or supplementary-plane characters, split('') breaks them in half. Spread iterates code points correctly.
map[ch] || ch â anything not in the map (spaces, digits, punctuation, emoji) falls through unchanged. That preserves whitespace and any pre-existing symbols in the input.
Unicode escapes use \u{...} syntax â the curly braces are required for code points above U+FFFF, which is where most of the mathematical alphabets live. \u1D41A (without braces) does not work â it parses as \u1D41 followed by the literal string A.
Adding more styles
Every style is just another map. Italic:
const ITALIC = {
a: '\u{1D44E}', b: '\u{1D44F}', c: '\u{1D450}', // ...
h: '\u{210E}', // exception â h has a legacy code point
// ...
};
Watch out for that italic h (â, U+210E). Unicode reserves U+1D455 but never assigned it â the character was already defined elsewhere as "Planck constant" and Unicode does not duplicate. Same trap exists in script (e, g, o), Fraktur (C, H, I, R, Z), and double-struck (C, H, N, P, Q, R, Z). Your mapping table must reach into the Letterlike Symbols block (U+2100âU+214F) for these exceptions or you will emit undefined characters.
Combining marks â a different mechanism
Bold, italic, and script are alphabet substitutions. Strikethrough and underline work differently: they use combining marks that stack on top of a base character.
function strikethrough(text) {
return [...text].map(ch => ch === ' ' ? ' ' : ch + '\u0336').join('');
}
strikethrough('cancelled');
// 'cĖķaĖķnĖķcĖķeĖķlĖķlĖķeĖķdĖķ'
U+0336 is Combining Long Stroke Overlay. It has zero visual width â it renders on top of the character that immediately precedes it. The same pattern gives you underline (U+0332), overline (U+0305), dot above (U+0307), and dozens of others.
Spaces get skipped in the map because combining marks on spaces render as isolated diacritics floating in whitespace, which looks broken.
Handling paste-in from users
If you accept text from a user, they may paste something that already contains styled characters. Trying to re-map ððŪðĩðĩðļ through your bold-serif table produces garbage because your table only knows about basic Latin letters.
The pragmatic fix is normalizing to plain ASCII before converting:
function normalize(text) {
const RANGES = [
[0x1D400, 0x1D433], // bold serif
[0x1D434, 0x1D467], // italic
[0x1D468, 0x1D49B], // bold italic
// ...add ranges as needed
];
return [...text].map(ch => {
const code = ch.codePointAt(0);
for (const [start] of RANGES) {
if (code >= start && code < start + 52) {
const offset = (code - start) % 26;
const isUpper = (code - start) < 26;
return String.fromCharCode((isUpper ? 65 : 97) + offset);
}
}
return ch;
}).join('');
}
This reverses the mapping â anything within a known mathematical alphabet range gets pulled back to plain AâZ or aâz before you re-convert.
Where the ecosystem sits
Every Unicode font tool on the internet â the kind users hit when searching for a font generator â runs some variation of the pattern above. The differentiation is not in the algorithm, it is in the curation. Which combinations of alphabet substitution and combining marks and symbol frames produce styles people actually want to copy.
I built livefontgenerator.com around this exact code, and after a few thousand daily users the interesting engineering problems all turn out to be UX ones â how do you show 200+ styles without overwhelming the input latency, how do you organize them so users find their aesthetic in under ten seconds, how do you handle the copy-paste flow on mobile Safari (spoiler: navigator.clipboard.writeText is finally reliable in 2026, but a fallback still helps).
If you want to see how different organizational approaches feel, the same core converter runs three different UIs at font style generator (category tabs) and fancy text generator (randomized frames) â same 30 lines of JavaScript under the hood, completely different products from a user's perspective.
The accessibility caveat
One thing worth flagging if you ship this to real users: screen readers do not treat mathematical alphabet characters as regular letters. NVDA and JAWS often read them character-by-character with their full Unicode name, which makes styled bios essentially unreadable for blind users.
For a fun tool this is fine â the styled text is the product. But if you build this into anything that matters for accessibility (form labels, headings, meaningful content), use CSS font-weight and font-style instead. Real formatting for real information, Unicode tricks for decoration.
Try it
Copy the BOLD_SERIF map and convert function above into a browser console right now. Paste some text. It just works.
That is the entire technology behind an industry of "font generators" â one lookup table, one .map().join(''), and a couple of hundred thousand daily users who have no idea their fancy bio is really six code points from a math paper.

Top comments (0)