Kaomoji rendering cases
Where ordinary font fallback cannot preserve the intended face. Pick a concept to open its structure; stored and copied text never changes.
Solid: needs · dashed: outcome of
Only the code points travel when a face is copied. Neither treatment does, so the destination decides the appearance again from the top of this graph.
Bytes, code points, clusters, glyphs
| Layer | What it is | ゚U+FF9F at that layer |
|---|---|---|
| Bytes, in UTF-8 | how it is stored and sent | EF BE 9F |
| Code point | the number Unicode assigns | U+FF9F |
| Grapheme cluster | what a reader counts as one character | one — but U+FF65 + U+0306 is two |
| Glyph | the shape a font draws | ゚ — or □ where the font has none |
this block’s script · Common · Inherited · another script · unassigned · unassigned, with a note · hover a cell to read it
| Encoding | Unit | AU+0041 | ゚U+FF9F | 😀U+1F600 |
|---|---|---|---|---|
| UTF-8 | 1–4 bytes | 41 | EF BE 9F | F0 9F 98 80 |
| UTF-16 | 1–2 units of 16 bits | 0041 | FF9F | D83D DE00 |
| UTF-32 | Always 4 bytes | 0000 0041 | 0000 FF9F | 0001 F600 |
UTF-8 keeps ASCII one byte per character, which is why it won the wire. UTF-16 needs a surrogate pair above U+FFFF, which is why a JavaScript string can report a length of 2 for one emoji.
| Where | Unit | Consequence here |
|---|---|---|
| HTTP responses, stored JSON, this page | UTF-8 bytes | Declared by charset=utf-8 on the response, not guessed. |
| The renderer, in the browser | UTF-16 code units | Array.from(text) walks code points; indexing would halve anything above U+FFFF. |
| The server, in Python | Code points | len('゚') is 1 whatever the encoding on disk. |
| The share-card PNGs | Glyphs | Rendered by pango against this server's fonts, so coverage there is fixed rather than per visitor. |
Script property: why a face resolves as one run
The procedure is UAX #24. Almost every character a kaomoji is built from is Common, so a face usually resolves to a single font.
Two lines of Scripts.txt, quoted exactly. Select a part to read what it is.
# @missing: 0000..10FFFF; Unknown
The format is not specific to this file: UAX #44 defines how every file in the character database is laid out — which fields exist, how ranges and comments are written, and what a missing value defaults to. It is long because it governs the whole database; the two sections that explain a line like these are Format Conventions and Data Fields. The rules for this particular property are in UAX #24 instead.
| Step | Where it happens | What exists at that step |
|---|---|---|
| Published | The UCD, as a line in a text file | FF9E..FF9F ; Common # Lm [2] |
| Compiled | Built into the shaping library — ICU, HarfBuzz, the browser | The ranges become a lookup table, so a query is an index rather than a search |
| Queried | Itemization, once per character | script(U+FF9F) → Common |
| Defaulted | Anything the file does not list | Unassigned code points answer Unknown; Script_Extensions falls back to the Script value |
Scripts-17.0.0.txt assigns all 159,866 designated code points in 2,287 rows across 174 script values, of which Common alone accounts for 9,123 code points in 613 ranges. The table ships inside whichever library performs the lookup, so it is pinned to the Unicode version that library was built against — two browsers of different vintages can answer the same query differently.
| Code point | Script | Script_Extensions | Shaping run |
|---|---|---|---|
| (U+0028 | Common | — | Run 1 · font chosen for the parenthesis |
| ゚U+FF9F | Common | Hira Kana | Run 1 · no glyph in that font → .notdef |
| οU+03BF | Greek | — | Run 2 · a real script, so it breaks the run |
| ゚U+FF9F | Common | Hira Kana | Run 3 · opens the run, so fallback finds a Japanese font |
| )U+0029 | Common | — | Run 3 |
The pair is grouped by run itemization, not by any attachment of its own: U+FF9F is a spacing modifier letter (gc=Lm), not a combining mark. Measured in Chrome, (゚ reports one advance for the two characters and ο゚ reports two.
| Value | ISO 15924 | Behaviour during itemization | Characters in these cases |
|---|---|---|---|
| Common | Zyyy | Owns no script; joins whichever run its neighbours form. | (U+0028)U+0029-U+002D゚U+FF9E-FF9F・U+FF61-FF65⑉U+2449◡U+25E1 |
| Inherited | Zinh | Takes the script of the preceding character, whatever that turns out to be. | ◌̆U+0306◌̑U+0311 |
| A named script | Grek, Thai, Kana | Starts a run, and ends whatever run preceded it. | οU+03BF๑U+0E51カU+FF66-FF9D |
| Script | ISO 15924 | Seen as | Uses | Distinct | Assigned | What it asks for |
|---|---|---|---|---|---|---|
| Common | Zyyy | (U+0028゚U+FF9F | 3,625 | 163 | 9,123 | No script of its own: joins whichever run its neighbours form. |
| Inherited | Zinh | ◌̆U+0306 | 256 | 12 | 684 | Takes the run of the character before it, and is drawn over that glyph. |
| Katakana | Kana | ツU+30C4 | 178 | 11 | 321 | No case, no joining — but only a Japanese family carries it. |
| Latin | Latn | bU+0062 | 155 | 20 | 1,492 | Cased. Whatever the browser calls sans-serif almost always covers it. |
| Thai | Thai | ๑U+0E51 | 122 | 5 | 86 | Written without word spaces; vowels and tones stack above and below the consonant. |
| Greek | Grek | οU+03BF | 105 | 7 | 518 | Cased, and enough of a script to end a Common run — which is what saved the second eye. |
| Hiragana | Hira | つU+3064 | 73 | 7 | 381 | Same font requirement as Katakana; the two share Script_Extensions. |
| Arabic | Arab | وU+0648 | 55 | 6 | 1,413 | Right to left and cursive: each letter has up to four contextual forms. |
| Hangul | Hang | ㅅU+3145 | 27 | 5 | 11,739 | Jamo compose into syllable blocks, so glyph count far exceeds letter count. |
| Canadian Aboriginal | Cans | ᕕU+1555 | 25 | 11 | 726 | Syllabics, where rotation of one shape is a different sound. |
| Kannada | Knda | ಠU+0CA0 | 20 | 2 | 92 | Reordering and conjuncts: stored order and drawn order differ. |
| Cyrillic | Cyrl | ДU+0414 | 10 | 3 | 508 | Cased, and shares many shapes with Latin without sharing code points. |
| Han | Hani | 人U+4EBA | 10 | 5 | 103,351 | The largest script in Unicode; no case, no joining, one advance per character. |
| Georgian | Geor | აU+10D0 | 2 | 2 | 173 | Unicameral in traditional use, with a separate uppercase style added later. |
Measured across the 666 distinct faces in the catalogue at the time of writing, with every character mapped through Scripts.txt; Assigned is how many code points that script holds in Unicode overall. Kaomoji raid scripts for shapes, not for language, so the tail is long and thin: Kannada supplies two characters and both are eyes. The catalogue leans on Common because punctuation and symbols live there, and that is exactly the value that cannot open a run of its own.
The half-width block straddles those values—U+FF61-FF65 and U+FF9E-FF9F are Common while the letters between them are Katakana—so the renderer keys its face class on the block range U+FF61-FF9F rather than on the script.
| Standard | Defines | What it says about U+FF9F |
|---|---|---|
| ISO/IEC 10646 | The repertoire: which characters exist, their names and code points. | FF9F ; HALFWIDTH KATAKANA SEMI-VOICED SOUND MARK |
| The Unicode Standard | The same repertoire plus the properties and algorithms built on it, published as the Unicode Character Database — plain text files such as UnicodeData.txt, Scripts.txt and ScriptExtensions.txt, which every shaping library compiles into its own tables. | Script=Common · scx=Hira Kana |
| ISO 15924 | Four-letter script codes, with Unicode as registrar. | Zyyy · Kana |
The two character standards are kept synchronized and assign the same code points, so a renderer bug is rarely a question of which one you consult. It matters here only because the run split is a property question, and properties live on the Unicode side.
From Latin accent to kaomoji eyebrow
A breve is the small curved accent ˘. In Latin-based writing it is commonly placed above a vowel—a + ◌̆ → ă—often to mark a short vowel sound. Its inverted counterpart is an arch rather than a cup, but it is still positioned above the base character: a + ◌̑ → ȃ.
Both U+0306 and U+0311 have Unicode canonical combining class 230: above. The inverted breve belongs to specialist notation rather than an everyday alphabet. “Inverted” changes the curve from ◌̆ to ◌̑; it does not move the mark below the letter. A genuinely below-positioned mark has a different code point, such as U+032F ◌̯.
Unicode stores the letter and accent as separate code points, and they stay separate: nothing merges them into a third code point. Combining describes what the renderer does with the second one — the mark has no advance width of its own, so its glyph is positioned over the glyph before it, and two code points come out as one drawn shape: U+0061 + U+0306 → ă. Where exactly the mark lands is the font’s business: it carries anchor points for the pairs it knows, and guesses for the rest.
Kaomoji composition borrows character shapes without preserving their original linguistic meanings. Here, U+FF65 HALFWIDTH KATAKANA MIDDLE DOT supplies the eye and the breve supplies the eyebrow: ・ + ◌̆ → ・̆. This imports the visual relationship “small curve above another character” from Latin typography, then reassigns it from pronunciation to facial expression. Other characters in the same faces similarly become cheeks and mouths despite originating as OCR symbols, Thai digits, or geometric marks.
Font coverage: no family paints a whole face
--kaomoji-font is a CSS custom property naming the first font asked for a face, not the font that paints every character in it. Where the named font has nothing, fallback answers character by character, so a face normally arrives from several fonts at once. The token decides only which characters are answered without falling back — and that is the whole of what the treatment controls.
| Code point | Block | DejaVu Sans | Noto Sans | Noto Sans CJK JP | Noto Sans Symbols2 | Noto Sans Thai | Noto Color Emoji | Falls to |
|---|---|---|---|---|---|---|---|---|
| (U+0028 | Basic Latin | ✓ | ✓ | ✓ | · | · | · | DejaVu Sans |
| οU+03BF | Greek and Coptic | ✓ | ✓ | ✓ | · | · | · | DejaVu Sans |
| ‿U+203F | General Punctuation | ✓ | ✓ | · | · | · | · | DejaVu Sans |
| ◕U+25D5 | Geometric Shapes | ✓ | · | · | ✓ | · | · | DejaVu Sans |
| ⑉U+2449 | Enclosed Alphanumerics | · | · | · | ✓ | · | · | Noto Sans Symbols2 |
| ๑U+0E51 | Thai | · | · | · | · | ✓ | · | Noto Sans Thai |
| ・U+FF65 | Halfwidth Forms | · | · | ✓ | · | · | · | Noto Sans CJK JP |
| ゚U+FF9F | Halfwidth Forms | · | · | ✓ | · | · | · | Noto Sans CJK JP |
| ◌̆U+0306 | Combining Diacriticals | ✓ | ✓ | · | · | · | · | DejaVu Sans |
| 🔥U+1F525 | Misc Symbols and Pictographs | · | · | · | · | · | ✓ | Noto Color Emoji |
Two intersections are empty, which is why both failures on this page are structural rather than unlucky — no installed family covers both ・U+FF65 and ◌̆U+0306, so that pair must arrive from two fonts and its mark has no anchor to share; and none covers both ゚U+FF9F and ◕U+25D5, so a stack that answers the Katakana eyes cannot also answer the geometric ones. The last column is what fontconfig actually picks, which is the first family in its order that covers the character, not necessarily the only one that could. These are this server’s installed fonts, the set pango resolves for the PNG share previews; a visitor’s browser runs the same algorithm over whatever their system has.
| Face | Half-width Katakana | --kaomoji-font | Answered by the named font | Answered by fallback |
|---|---|---|---|---|
| (゚ο゚) | Two U+FF9F | --kaomoji-katakana-font | (U+0028゚U+FF9FοU+03BF゚U+FF9F)U+0029 | — |
| (◕‿◕) | None | sans-serif | (U+0028◕U+25D5‿U+203F◕U+25D5)U+0029 | — |
| (◕‿◕) | None — if the stack were applied to every face | --kaomoji-katakana-font | (U+0028)U+0029 | ◕U+25D5‿U+203F |
Hence the treatment is opt-in per face rather than applied to all of them. A face carrying the half-width block has no alternative: only a Japanese family can paint those eyes, so it accepts whichever one the viewer happens to have. A face the generic already covers gains nothing and loses the guarantee — its geometric shapes stop being answered directly and are re-answered by fallback, at whatever advance the substituting font uses. Faces are laid out against the generic (white-space: nowrap, fixed min-width, clamp() sizes), so a substitution that tightens those advances can print a face with its eyes and mouth on top of each other.
| # | Family | Typically installed on | Present on this server |
|---|---|---|---|
| 1 | Hiragino Kaku Gothic ProN | macOS, iOS | No |
| 2 | Hiragino Sans | macOS (newer releases) | No |
| 3 | Yu Gothic | Windows 8.1 and later | No |
| 4 | Meiryo | Windows Vista and later | No |
| 5 | Noto Sans CJK JP | Linux, Android, ChromeOS | Yes — the one that answers here |
| 6 | Noto Sans JP | Google Fonts; some Linux images | No |
| 7 | sans-serif | Everywhere, as the generic | Yes (DejaVu Sans) — and it is why the box appeared |
One name per platform, because none of them is portable and the app ships no web font: the stack asks each major system for the Japanese family it already has, and the browser takes the first that exists. A viewer with none of the six lands on the generic in position 7 and sees the box again — the treatment raises the odds, it does not guarantee coverage. Presence measured here with fc-list; the platform column is where each family normally ships, not a measurement.
The class rebinds the custom property rather than setting font-family, so it reaches surfaces whose own stylesheet writes the font: shorthand: the value they read changes with them.
Live renderer registry
| Example | Unicode cluster | Failure | Rendering treatment |
|---|---|---|---|
| Loading renderer registry… | |||
Exact DOM and CSS treatment
Each treatment in the registry resolves to the following selectable markup and rules.