Optimized, subsetted Fluent Emoji webfonts for the web. Built as unicode-range-split woff2 chunks so browsers only download the glyphs actually used on a page. Some work and inspiration from tetunori/fluent-emoji-webfont.
I like emojis. They are an underrated art form. I love the ones from Microsoft, and I want to use them in my work and personal projects.
But... how do you do it? Existing solutions that monkey-patch emojis client-side via JavaScript are finicky (they're hacks). I want something clean, with minimal network roundtrips for each emoji.
Emoji fonts are the solution. Browsers can use them, but there are some considerations:
-
Emoji fonts are large. Yes, some people ship 5MB+ JavaScript bundles to render buttons, but not everyone. Most web users appreciate smaller downloads.
-
Emoji fonts aren't properly standardized. COLRv1 works in Chrome and Firefox; OT-SVG works in Safari and Firefox. There's a gap. We can't pick just one, we need both.
-
Seems like we can merge both standards into the same font file. We are increasing the problem about size, though...
-
There are other standards, like something related to bitmaps (I don't remember exactly. Yes, I'm not an LLM writing this), which should work universally. The problem is, these usually result in huge files and they are rasterized graphics at the end of the day. They don't scale with resolution.
Enter @font-face with tech() and unicode-range! Don't you love CSS?
tech(): the browser automatically picks the emoji standard it supports.unicode-range: we subset fonts into chunks. If your page has one emoji, the browser downloads only the chunk containing it.
Get the latest fonts from the releases page.
Include the CSS for the style you want:
<!-- Color style (with gradients, 3D style) -->
<link rel="stylesheet" href="path/to/dist/color/FluentEmojiColor.css">
<!-- Or Flat style (solid colors) -->
<link rel="stylesheet" href="path/to/dist/flat/FluentEmojiFlat.css">Then use it in your CSS:
p {
font-family: sans-serif, 'Fluent Emoji Color';
/* or: font-family: sans-serif, 'Fluent Emoji Flat'; */
}Always place the text font first — the emoji font is a fallback. The browser uses it only for codepoints not found in the primary font.
As mentioned, @font-face with tech() hints to automatically serve the right format, and with unicode-range it only fetches the chunk needed to render the specific glyph (emoji):
@font-face {
font-family: 'Fluent Emoji Color';
src:
url('colrv1/chunk-000.woff2') format('woff2') tech(color-COLRv1),
url('otsvg/chunk-000.woff2') format('woff2') tech(color-SVG);
unicode-range: U+1F600, U+1F601, U+1F602;
font-display: swap;
}- Chrome & Firefox → load COLRv1 (~1.2 MB total, ~26 KB per chunk)
- Safari → load OT-SVG (~2.5 MB total, ~52 KB per chunk)
dist/
color/
colrv1/ # COLRv1 woff2 chunks (Chrome, Firefox)
otsvg/ # OT-SVG woff2 chunks (Safari)
FluentEmojiColor.css
flat/
colrv1/ # COLRv1 woff2 chunks
otsvg/ # OT-SVG woff2 chunks
FluentEmojiFlat.css
macOS:
brew install uv ninjaArch Linux:
pacman -S python uv ninjaOr install uv via the official installer: curl -LsSf https://astral.sh/uv/install.sh | sh
./build.sh
# 20 ~ 60 minutes to buildThe build script will:
- Create a venv and install dependencies (
nanoemoji,picosvg,fonttools,brotli) - Prepare SVGs — map unicode codepoints from metadata, symlink to nanoemoji-compatible filenames
- Compile 4 fonts via nanoemoji (COLRv1 + OT-SVG for each style)
- Fix font metrics on each TTF to match Apple Color Emoji baselines
- Subset each font into ~30-glyph woff2 chunks via
pyftsubset - Generate CSS with
@font-facerules,unicode-range, andtech()hints
| Script | Purpose |
|---|---|
prepare.py |
Walks fluentui-emoji assets, reads metadata.json for unicode mappings, creates symlinked SVGs with nanoemoji-compatible names |
build.sh |
Orchestrates the full pipeline: deps, prepare, compile, fix metrics, subset, CSS |
fix_metrics.py |
Post-processes compiled TTFs to match Apple Color Emoji metrics (advance width, vertical metrics) |
generate_css.py |
Reads chunk ranges and emits @font-face rules with tech() hints |
Fluent Emoji SVGs live in fluentui-emoji with descriptive filenames. Each emoji directory contains a metadata.json with unicode codepoint(s), including ZWJ sequences and skin tone variants.
prepare.py reads these and creates symlinks named emoji_uXXXX.svg (the format nanoemoji expects). nanoemoji then compiles them into OpenType color fonts, automatically running picosvg normalization on the SVGs.
The compiled fonts are split into unicode-range chunks and compressed to woff2. The generated CSS uses the tech() function so Chrome/Firefox get COLRv1 and Safari gets OT-SVG, all under the same font-family name.
Some Color SVGs have <mask> elements that picosvg can't normalize. These are automatically filtered out during the prepare step and fall back to the system emoji font.
SVGs from microsoft/fluentui-emoji (MIT License).