Kaomojo

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

Code point Script property Font coverage Combining mark Run itemization Font selection Shaping No glyph Detached mark Japanese stack CSS curve Regression

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

The same eye at each layer
LayerWhat it isU+FF9F at that layer
Bytes, in UTF-8how it is stored and sentEF BE 9F
Code pointthe number Unicode assignsU+FF9F
Grapheme clusterwhat a reader counts as one characterone — but U+FF65 + U+0306 is two
Glyphthe 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

Encodings: the same code points, packed differently
EncodingUnitAU+0041U+FF9F😀U+1F600
UTF-81–4 bytes41EF BE 9FF0 9F 98 80
UTF-161–2 units of 16 bits0041FF9FD83D DE00
UTF-32Always 4 bytes0000 00410000 FF9F0001 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.

Which layer each part of this app works in
WhereUnitConsequence here
HTTP responses, stored JSON, this pageUTF-8 bytesDeclared by charset=utf-8 on the response, not guessed.
The renderer, in the browserUTF-16 code unitsArray.from(text) walks code points; indexing would halve anything above U+FFFF.
The server, in PythonCode pointslen('゚') is 1 whatever the encoding on disk.
The share-card PNGsGlyphsRendered 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.

How U+FF9F gets a Script value
StepWhere it happensWhat exists at that step
PublishedThe UCD, as a line in a text fileFF9E..FF9F ; Common # Lm [2]
CompiledBuilt into the shaping library — ICU, HarfBuzz, the browserThe ranges become a lookup table, so a query is an index rather than a search
QueriedItemization, once per characterscript(U+FF9F) → Common
DefaultedAnything the file does not listUnassigned 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.

Itemization of (゚ο゚)
Code pointScriptScript_ExtensionsShaping run
(U+0028CommonRun 1 · font chosen for the parenthesis
U+FF9FCommonHira KanaRun 1 · no glyph in that font → .notdef
οU+03BFGreekRun 2 · a real script, so it breaks the run
U+FF9FCommonHira KanaRun 3 · opens the run, so fallback finds a Japanese font
)U+0029CommonRun 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.

Script values across this registry
ValueISO 15924Behaviour during itemizationCharacters in these cases
CommonZyyyOwns no script; joins whichever run its neighbours form.(U+0028)U+0029-U+002DU+FF9E-FF9FU+FF61-FF65U+2449U+25E1
InheritedZinhTakes the script of the preceding character, whatever that turns out to be.◌̆U+0306◌̑U+0311
A named scriptGrek, Thai, KanaStarts a run, and ends whatever run preceded it.οU+03BFU+0E51U+FF66-FF9D
Scripts the catalogue borrows from, and what each asks of a renderer
ScriptISO 15924Seen asUsesDistinctAssignedWhat it asks for
CommonZyyy(U+0028U+FF9F3,6251639,123No script of its own: joins whichever run its neighbours form.
InheritedZinh◌̆U+030625612684Takes the run of the character before it, and is drawn over that glyph.
KatakanaKanaU+30C417811321No case, no joining — but only a Japanese family carries it.
LatinLatnbU+0062155201,492Cased. Whatever the browser calls sans-serif almost always covers it.
ThaiThaiU+0E51122586Written without word spaces; vowels and tones stack above and below the consonant.
GreekGrekοU+03BF1057518Cased, and enough of a script to end a Common run — which is what saved the second eye.
HiraganaHiraU+3064737381Same font requirement as Katakana; the two share Script_Extensions.
ArabicArabوU+06485561,413Right to left and cursive: each letter has up to four contextual forms.
HangulHangU+314527511,739Jamo compose into syllable blocks, so glyph count far exceeds letter count.
Canadian AboriginalCansU+15552511726Syllabics, where rotation of one shape is a different sound.
KannadaKndaU+0CA020292Reordering and conjuncts: stored order and drawn order differ.
CyrillicCyrlДU+0414103508Cased, and shares many shapes with Latin without sharing code points.
HanHaniU+4EBA105103,351The largest script in Unicode; no case, no joining, one advance per character.
GeorgianGeorU+10D022173Unicameral 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.

Where these facts are defined
StandardDefinesWhat it says about U+FF9F
ISO/IEC 10646The repertoire: which characters exist, their names and code points.FF9F ; HALFWIDTH KATAKANA SEMI-VOICED SOUND MARK
The Unicode StandardThe 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 15924Four-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.

Coverage signatures — which of these code points each family can draw, measured with fc-list :charset= on this server
Code pointBlockDejaVu SansNoto SansNoto Sans CJK JPNoto Sans Symbols2Noto Sans ThaiNoto Color EmojiFalls to
(U+0028Basic Latin···DejaVu Sans
οU+03BFGreek and Coptic···DejaVu Sans
U+203FGeneral Punctuation····DejaVu Sans
U+25D5Geometric Shapes····DejaVu Sans
U+2449Enclosed Alphanumerics·····Noto Sans Symbols2
U+0E51Thai·····Noto Sans Thai
U+FF65Halfwidth Forms·····Noto Sans CJK JP
U+FF9FHalfwidth Forms·····Noto Sans CJK JP
◌̆U+0306Combining Diacriticals····DejaVu Sans
🔥U+1F525Misc 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.

What the token is set to, and what it changes
FaceHalf-width Katakana--kaomoji-fontAnswered by the named fontAnswered by fallback
(゚ο゚)Two U+FF9F--kaomoji-katakana-font(U+0028U+FF9FοU+03BFU+FF9F)U+0029
(◕‿◕)Nonesans-serif(U+0028U+25D5U+203FU+25D5)U+0029
(◕‿◕)None — if the stack were applied to every face--kaomoji-katakana-font(U+0028)U+0029U+25D5U+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.

The stack the class rebinds to, in order
#FamilyTypically installed onPresent on this server
1Hiragino Kaku Gothic ProNmacOS, iOSNo
2Hiragino SansmacOS (newer releases)No
3Yu GothicWindows 8.1 and laterNo
4MeiryoWindows Vista and laterNo
5Noto Sans CJK JPLinux, Android, ChromeOSYes — the one that answers here
6Noto Sans JPGoogle Fonts; some Linux imagesNo
7sans-serifEverywhere, as the genericYes (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.