Every platform draws its own emoji. macOS and iOS use Apple Color Emoji, Windows uses Segoe UI Emoji, Android and most Linux distributions use Noto Color Emoji. Three fonts, three sets of artwork, three sets of metrics, for the same codepoints.
Draw a handful of emoji to a canvas and hash the pixels, and the hash names the platform. Not because of any lie in the fingerprint, but because the picture of a face on a Mac is a different picture from the face on a Windows machine, and the canvas returns pixels.
What makes this different from the general canvas fingerprint is the licensing. Apple Color Emoji is Apple’s, Segoe UI Emoji is Microsoft’s, and neither can be redistributed. A browser build cannot ship them the way it can ship a metric-compatible substitute for Segoe UI. The only one of the three that can travel is Noto Color Emoji, which is openly licensed - which means the one cross-OS persona that is fully achievable on the emoji dimension is Android, and the two most commonly claimed are not.
Naming the font does not select it
The first thing to get out of the way is that this is not an enumeration problem. Measured on macOS, asking for each of the three emoji fonts by name produced identical results:
| font stack requested | width at 32px | pixel hash |
|---|---|---|
32px sans-serif |
160 | 9636209a |
"Apple Color Emoji", 32px sans-serif |
160 | 9636209a |
"Segoe UI Emoji", 32px sans-serif |
160 | 9636209a |
"Noto Color Emoji", 32px sans-serif |
160 | 9636209a |
Every row got Apple Color Emoji, because that is what this host has and what emoji fallback resolves to. Naming a font the host does not have changes nothing.
So the signal is not “which emoji font is in the enumerable list”. It is the single hash in the right column, which is produced by whichever emoji font the host actually owns, and which no allow-list touches.
ZWJ sequences are the cheap version
Hashing pixels requires a reference set to compare against. There is a simpler measurement that needs no reference at all, and it comes from how modern emoji are constructed.
Many emoji are not one codepoint. They are sequences joined by U+200D, the zero-width joiner: a family is four person emoji joined together, a rainbow flag is a white flag joined to a rainbow, a professional is a person joined to an object. A font that supports the sequence composes it into one glyph. A font that does not falls back to rendering each component separately, and the result is several glyphs wide.
Measured on the same host:
| string | width at 32px |
|---|---|
| a single person emoji | 32 |
| a four-person family, ZWJ-joined | 32 |
| the same four people, unjoined | 128 |
| rainbow flag, ZWJ-joined | 32 |
| plain white flag | 32 |
One glyph, 32 pixels: the font composed it. The unjoined control at 128 shows what failure looks like. A host whose emoji font lacks a sequence, or whose emoji support is partial, returns something between the two, and a host with no color emoji font at all returns fallback boxes.
That is a self-contained check. No reference table, no per-platform baseline: compose a sequence, compare against the width of one emoji, and see whether the host’s font knows the sequence.
Where the versions come in
Emoji fonts are updated with the operating system, and each Unicode Emoji release adds sequences. So sequence support is a rough OS-version signal on top of the platform signal.
Take a set of sequences introduced across several Unicode Emoji revisions, measure each one against the single-glyph width, and the boundary between “composes” and “falls apart” tells you approximately how old the host’s emoji font is. A browser claiming a current Windows whose emoji font stops composing at a sequence from three years ago is describing a machine that has not been updated in three years - which may be true, and is a narrower bucket to be in either way.
Skin-tone modifiers are a second axis of the same kind. A base person emoji followed by a modifier composes on a font that supports it and renders as two glyphs on one that does not.
What this costs a persona
Line the three platforms up against what a browser build can actually do:
| claimed platform | emoji font needed | redistributable | achievable off-platform |
|---|---|---|---|
| Android or Linux | Noto Color Emoji | yes, OFL | yes |
| Windows | Segoe UI Emoji | no | no |
| macOS or iOS | Apple Color Emoji | no | no |
An Android persona can be made honest here by shipping the real font, the same way its text fonts can. A Windows persona on a Mac cannot, and the substitution trick that works for Segoe UI text does not transfer: there is no metric-compatible open substitute for an emoji font, and even if the metrics matched, the pixels are the signal.
That leaves three options, and it is worth being clear that all three are compromises. Expose the host’s emoji font, and the emoji dimension contradicts the claimed platform. Suppress color emoji entirely, and produce a browser that renders emoji as monochrome boxes, which is a rarer state than either platform. Or accept that the persona’s OS claim is only as good as the least fakeable dimension it touches, and choose the claim accordingly.
Reading it as a detector
The practical checks, cheapest first:
- ZWJ composition. Compose several sequences, compare against a single glyph. Needs no baseline and catches missing or partial emoji support outright.
- Rasterised hash. Draw a fixed emoji string, hash the pixels, compare against per-platform baselines. Highest confidence, needs a reference set you collect once.
- Metrics. Emoji advance widths and
fontBoundingBoxvalues differ between the three fonts, someasureTextgives a weaker version of the hash without touching pixels. - Consistency. The platform implied by the emoji font against the platform implied by the text fonts, the User-Agent and the interface census. Emoji and text fonts come from the same operating system image, and a browser whose text fonts say Windows while its emoji say macOS has answered the same question twice.