I built Unicode Text Tools, a free site with a bunch of text converters — superscript, subscript, bubble/circled text, upside-down text, small caps, and more. Type something, get it transformed, copy it out.
The whole engine is one dependency-free JS file built entirely from character mapping tables. No AI, no server, no libraries. Here's why that's the right architecture for this class of tool, and how the trickier conversions work.
The core idea: it's all just lookup tables
Every conversion on the site is a function that maps each input character to a Unicode character (or does a small transform). The simplest cases are pure dictionaries:
// Superscript (full a-z, 0-9)
var SUP = {
a: 'ᵃ', b: 'ᵇ', c: 'ᶜ', d: 'ᵈ', e: 'ᵉ', f: 'ᶠ', g: 'ᵍ', h: 'ʰ', i: 'ⁱ',
j: 'ʲ', k: 'ᵏ', l: 'ˡ', m: 'ᵐ', n: 'ⁿ', o: 'ᵒ', p: 'ᵖ', q: 'ᵠ', r: 'ʳ',
s: 'ˢ', t: 'ᵗ', u: 'ᵘ', v: 'ᵛ', w: 'ʷ', x: 'ˣ', y: 'ʸ', z: 'ᶻ',
'0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵',
'6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹', '+': '⁺', '-': '⁻', '=': '⁼',
'(': '⁽', ')': '⁾'
};
The transform itself is trivial — walk the string, look up each char, append the mapped value (or the original char if unmapped). The work is in the tables: knowing which Unicode blocks exist, what's 1:1 reversible, and what's incomplete.
The Unicode reality check
Here's the thing nobody tells you about Unicode text transformation: the blocks are inconsistent.
- Superscript: complete for a-z and 0-9 — fully reversible.
-
Subscript: incomplete — there's no subscript
b,c,d,f,g,q,w,y,z. If you map an input with those letters, you have to decide what to do with them. -
Small caps:
xhas no small-cap form (ꞯis the closest, but it's a different character and looks wrong).jis a problem too — the Unicode small-capᴊcollides visually with the small-capiin many fonts.
The site's approach: fallback keeps the original character. Unmappable input passes through unchanged rather than being silently corrupted:
function convert(text, table) {
return text.split('').map(ch => table[ch] || ch).join('');
}
This is the pragmatic contract — "I'll transform everything I can, and leave the rest alone" — and it's honest about Unicode's limits. A subscript c just stays c, and the user understands.
Uppercase reuse: a tiny DRY trick
The superscript and subscript blocks only define lowercase. But users type "HELLO" in all caps all the time. Instead of duplicating every entry, uppercase letters are folded onto their lowercase mapping at load time:
(function addUpper(tbl) {
Object.keys(tbl).forEach(function (k) {
if (k >= 'a' && k <= 'z') tbl[k.toUpperCase()] = tbl[k];
});
})(SUP);
One line, and the whole uppercase alphabet is covered without writing 26 more table entries.
The one that's not a lookup: upside-down text
Upside-down text is the interesting one, because it's not a pure per-character map. Flipping text 180° means you have to reverse the string order too — the first character visually ends up at the bottom-right.
var FLIP = {
a: 'ɐ', b: 'q', c: 'ɔ', d: 'p', e: 'ǝ', f: 'ɟ', g: 'ƃ', h: 'ɥ', i: 'ı',
j: 'ɾ', k: 'ʞ', l: 'l', m: 'ɯ', n: 'u', o: 'o', p: 'd', q: 'b', r: 'ɹ',
s: 's', t: 'ʇ', u: 'n', v: 'ʌ', w: 'ʍ', x: 'x', y: 'ʎ', z: 'z',
A: '∀', B: 'ᗺ', C: 'Ɔ', D: 'ᗡ', E: 'Ǝ', F: 'Ⅎ', G: '⅁', H: 'H', I: 'I',
J: 'ſ', K: 'ʞ', L: '⅂', M: 'W', N: 'N', O: 'O', P: 'Ԁ', Q: 'Ό', R: 'ᴚ',
S: 'S', T: '⊥', U: '∩', V: 'Λ', W: 'M', X: 'X', Y: '⅄', Z: 'Z',
'?': '¿', '!': '¡', '(': ')', ')': '(', '[': ']', ']': '[', '&': '⅋', '_': '‾'
};
Notice the map is symmetric — b flips to q, and q flips back to b. Making the map symmetric means the transform is a true involution: flip the text twice and you get the original back. That's a great property for a tool — users can copy flipped text and un-flip it to verify.
The symmetry is enforced programmatically, so the table can't drift:
// Make the flip map symmetric so upside-down text reverses exactly.
Object.keys(FLIP).forEach(function (k) {
var v = FLIP[k];
if (v !== k && !Object.prototype.hasOwnProperty.call(FLIP, v)) FLIP[v] = k;
});
And the transformation itself reverses the string:
function flipText(s) {
return s.split('').reverse().map(function (ch) {
return FLIP[ch] || ch;
}).join('');
}
Bubble text: computed, not tabulated
Bubble/circled text is the one case where a range-based calculation beats a hand-written table. The Unicode circled letters are in contiguous blocks, so they're computed on the fly:
function bubbleChar(ch) {
var code = ch.charCodeAt(0);
if (code >= 97 && code <= 122) return String.fromCharCode(0x24D0 + (code - 97)); // ⓐ-ⓩ
if (code >= 65 && code <= 90) return String.fromCharCode(0x24B6 + (code - 65)); // Ⓐ-Ⓩ
if (code >= 48 && code <= 57) {
if (ch === '0') return '⓪';
return String.fromCharCode(0x2460 + (code - 49)); // ①-⑨
}
return ch;
}
One quirk worth noting: circled 0 is ⓪, not Ⓞ — the circled digits block starts at ①, and zero lives in a different Unicode block. If you'd written a generic 0x24EA + (code - 48) you'd have produced a circled letter O instead of a circled digit. These are the details that separate a polished tool from a broken one.
Architecture takeaways
- Character maps are the right tool for deterministic text transforms. They're fast, predictable, and debuggable — you can read the entire behavior of the engine by reading one table.
-
Fallback = keep the original. When Unicode doesn't have a mapping (subscript
c, small-capx), pass the character through rather than corrupting it. - A transform that's reversible is a feature. Symmetric flip maps let users un-flip text to verify — and the enforced symmetry prevents table drift.
- Know your Unicode blocks. Circled-zero lives in a different block than circled-1-9. Contiguous ranges are computable, but verify the edge cases.
The live site is at unicodetexttools.com — superscript, subscript, bubble, small caps, upside down, and a dozen more, all in one dependency-free JS file.
Have you built a text-transform tool? What did Unicode surprise you with? Let me know in the comments.
Top comments (0)