Emoji Salon: An Experiment and Demo of Color Font with Color Palette
- Link: https://emojisalon.art/
- Github Repo: https://github.com/rutopio/EmojiSalon
Emoji Salon is an experimental side project for me to explore how the color palette in a COLR
format color font can be dynamically changed using CSS. In a font that follows OpenType COLR
specification, each character can be divided into multiple colored shapes, similar to layers. This setup theoretically allows users to override any of these colors with CSS, offering a new level of customization on website.
While this concept could be applied to regular fonts, which might involve adjustments to shadows or fill colors, it becomes even more interesting with Emoji. By using this approach, it’s possible to completely recolor Emoji. This project seeks to test the limits of color customization within Emoji.
Relation Between Glyph and Color
Fonts are typically stored as vectors, which use Bézier curves to define the shape of glyph. While vector fonts only include the outlines of characters, their actual color is rendered by software.
The default color is often black due to the tradition of medieval manuscripts and early printing practices. Nowadays, most mainstream softwares allow for color changes, but since font files only include character coordinates without color attributes, changing the color of glyph means re-coloring the enclosed areas via the rendering and typesetting engine, rather than changing the design directly.
Typeface Revolution via Emoji
In 2010, Emoji became part of the Unicode 6.0 standard, marking a new era in font technology. This shift expanded the focus from monochrome font to color font.
Tech companies quickly saw the potential in adding color to text, leading to a competitive race to establish the best color standards for Emoji. This innovation laid the groundwork for the colorful digital expressions we see today.
Different Format Between Color Font
-
Bitmap: Apple SBIX & Google CBDT/CBLC
Apple'sSBIX
and Google'sCBDT/CBLC
replace Bézier curves with bitmap images linked to code points. However, these bitmap-based methods can result in distorted images when scaled and larger font files, making them less suitable for web and mobile use. -
Vector: Adobe-Mozilla OpenType-SVG
TheOpenType-SVG
standard allows embedding SVG graphics into code points, supporting gradients and animations. Each code point can link to one SVG file, but multiple styles for the same character require alternate glyphs. Despite Safari and Adobe apps support, it is less available for the web due to the lack of support from Chrome and Chromium-based browsers.

Trajan Color: Color font from Adobe Type. (Source: Adobe Typekit Blog )
-
Palette: Microsoft COLR/CPAL v0
Just like separated layers,COLR/CPAL
links colors and specific shapes in glyphs, reducing file size compared to image-based methods. It supports multiple palettes within a font, offering more flexibility thanOpenType-SVG
, allowing users to choose or customize colors. -
Palette: Microsoft-Google COLR/CPAL v1
Thev1
version ofCOLR/CPAL
update enhances thev0
format by supporting gradients and reusing paths to save space. This version improves design efficiency and reduces file size by referencing the same elements, rather than storing them separately.

Shape reuse and gradient color in the crystal ball emoji. (Source: Chrome for Developers)
Palette Customization Demo
For example, Rocher Color, designed by David Jonathan Ross, is a COLR/CPAL
color font that include 11 built-in color palettes. By default, it uses the first palette, which features an orange-brown color palette. Users can customize their color by using CSS properties base-palette
, allowing to select different palettes, such as base-palette: 1
for a pink palette or base-palette: 7
for a mint color palette.
// Default palette: orange-brown color
.class {
font-palette: --Default;
}
@font-palette-values --Default {
font-family: Rocher;
base-palette: 0;
}
// 1st palette: pink
.class {
font-palette: --Pink;
}
@font-palette-values --Pink {
font-family: Rocher;
base-palette: 1;
}
This feature gives users significant control over color presentation, making color fonts more flexible.
Functional Requirements Overview
Based on the comparisons mentioned above, people who want to use color fonts should use the COLR/CPAL
format for widely browser compatibility. Without this, there is a risk that Emojis may not display correctly both in Chrome (Blink kernel) or Safari (WebKit kernel).

Different color font format between browsers. (Source: ChromaCheck by PixelAmbacht)
First, I considered using Noto Color Emoji, which supports advanced color features like gradients and reusable components in the COLR/CPAL v1
format. However, I encountered an issue: Safari can not render COLR/CPAL v1
fonts ?!
I even made an PR for Can I use... (#6883) to report the support issue on iOS 17+.
All in all, I decided to use Twemoji, which Twitter designed and Mozilla repackaged into the more universally supported COLR/CPAL v0
format. This ensures Emojis are displayed correctly across all major browsers.
Emoji Picker: Emoji Mart
The page also needs an Emoji Picker for users to select Emojis. I plan to integrate Emoji Mart, which provides the Emoji Picker components and additional data to function effectively.
We can use the following code to fetch data from a CDN:
import { Picker } from "emoji-mart";
new Picker({
data: async () => {
const response = await fetch(
"https://cdn.jsdelivr.net/npm/@emoji-mart/data"
);
return response.json();
},
});
Or bundle the data directly from local data:
import { Picker } from "emoji-mart";
import data from "@emoji-mart/data";
new Picker({ data });
Options for Customization
The emoji-mart
package provides several configuration options for setting up an Emoji Picker:
data
: Define the source of the Emoji data. We can either fetch it remotely or include it directly in your bundle.onEmojiSelect
: Trigger the specific function whenever a user selects an Emoji.emojiVersion
: Specify the version of Emoji data being used, matching Unicode’s version numbering. For example,Unicode 15.1
corresponds toEmoji 15.1
.set
: Determine which Emoji style set to use, with options including native device styles or sets fromapple
,facebook
,google
, ortwitter
. For this project, I usetwitter
to match the Twemoji style.

Emoji Selection
When a user selects an Emoji, the onEmojiSelect
callback provides detailed information about that Emoji.
For instance, after selecting dolphin Emoji 🐬
, it would receive:
const pickerOptions = { onEmojiSelect: (res, _) => console.log(res) };
The output provides various details such as the name, the Unicode code point, and keywords associated with Emoji:
{
"id": "dolphin",
"name": "Dolphin",
"native": "🐬",
"unified": "1f42c",
"keywords": [
"flipper",
"animal",
"nature",
"fish",
"sea",
"ocean",
"fins",
"beach"
],
"aliases": ["flipper"]
}
Font Unpacking Tool: Fontkit
There are several tools for unpacking fonts, such as fontTools (Python) and Fontkit (JavaScript). Since the website is working on the front end, I use Fontkit to unpack the font file and read color palette information from the COLR/CPAL
table. To install Fontkit via npm:
npm install Fontkit
OpenType Tables
An OpenType font file consists of various data structures called tables (表). These tables store different types of information for font data and rendering. For example, in Noto Color Emoji, metadata like the font’s name, vendor, copyright, and version are stored in the name
table:
const fontkit = require("fontkit");
const fontURL =
"https://fonts.gstatic.com/s/notocoloremoji/v25/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.8.woff2";
// Fetch font from remote
async function loadFont(fontPath) {
const response = await fetch(fontPath);
const arrayBuffer = await response.arrayBuffer();
const buf = new Buffer(arrayBuffer);
const font = fontkit.create(buf);
return font;
}
var font = loadFont(fontURL);
console.log(font.name);
// Output: {version: 0, count: 8, stringOffset: 102, records: {…} ...}
console.log(font.name.records.fullName.en);
// Output: Noto Color Emoji
console.log(font.name.records.copyright.en);
// Output: Copyright 2022 Google Inc.
Features related to text substitutions, like the ccmp
feature used for Emoji composition, are found in the GSUB
table:
console.log(font.GSUB);
// Output: {version: 65536, scriptList: Array(2), featureList: Array(1), lookupList: LazyArrayValue ...}
console.log(font.GSUB.featureList[0].tag);
// Output: ccmp
For color fonts, the COLR
and CPAL
tables contain color information:
console.log(font.CPAL);
// Output: {version: 0, numPaletteEntries: 1356, numPalettes: 1, numColorRecords: 1356 ...}
Demo
For demo, let's create a simple page displaying the Dolphin Emoji 🐬
:
<div class="dolphin">🐬</div>
<style>
@font-face {
font-family: "Noto Color Emoji";
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/l/font?kit=Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diywYkdG2YmD0U&skey=a373f7129eaba270&v=v25)
format("woff2");
}
body {
--google-font-color-notocoloremoji: colrv1;
}
.dolphin {
font-family: "Noto Color Emoji", sans-serif;
}
</style>
This will render a 🐬
Emoji on localhost.
By importing Fontkit, it can directly unpacking the font by:
const fontPath =
"https://fonts.gstatic.com/l/font?kit=Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diywYkdG2YmD0U&skey=a373f7129eaba270&v=v25";
const response = await fetch(fontPath);
const arrayBuffer = await response.arrayBuffer();
const buf = new Buffer(arrayBuffer);
const font = Fontkit.create(buf);
console.log(font.CPAL.colorRecords);
// Output: (4) 0: {blue: 166, green: 109, red: 0, alpha: 255, parent: {…}, …}, ...
From this, we have known that:
- The Dolphin
🐬
is composed of four colors. - Each color has an associated layer index.
Using the override-colors
property in CSS, users can modify these colors. For instance, change layer index 2
from rgba(54, 180, 225, 255)
to rgba(93, 172, 129, 1)
:
.dolphin {
font-palette: --customize;
}
// Override original palette
@font-palette-values --customize {
font-family: Noto Color Emoji;
override-colors: 2 rgba(93, 172, 129, 1);
}
This results in a green Dolphin!
Color Picker
Next, it is time to build a Color Picker to allow users to select colors manually. Add a new element for the Color Pickers in HTML:
<style id="palette-overrides"></style>
<div id="color-pickers"></div>
Then, create Color Pickers for each color in the palette metadata:
const colorPickers = document.getElementById("color-pickers");
originalPalette.forEach((rgbaColorArray, idx) => {
// Create an Picker for different colors
const picker = document.createElement("input");
picker.type = "color";
picker.value =
"#" +
rgbaColorArray // '#' Represent hex color
.slice(0, 3) // Do not need alpha value
.map((ele) => ele.toString(16)) // To hex
.map((ele) => (ele.length == 1 ? "0" + ele : ele)) // Padding O to six digits
.join("");
colorPickers.appendChild(picker);
// Listen to Color Picker if user has any action,
// then trigger updateColorsPalette() function
picker.addEventListener("input", (event) => {
updateColorsPalette(idx, event.target.value);
});
});
Then update the CSS override-colors
property:
function updateColorsPalette(idx, colorValue) {
const style = document.getElementById("palette-overrides");
style.innerHTML = `
@font-palette-values --customize {
font-family: "Noto Color Emoji";
base-palette: 0;
override-colors: ${idx} ${colorValue};
}
`;
}
This lets users select and recolor new colors to the font's layers, giving a flexible way to customize Emoji and other color fonts.
Render Glyph on the Canvas
Simple Demo of HTML <canvas>
The <canvas>
element in HTML provides a powerful way to render glyph directly on page. To demo, let’s start with a simple rectangle. We define a canvas with a size of 300x300 pixels and give it a black border:
<canvas
id="myCanvas"
width="300"
height="300"
style="border:1px solid black"
></canvas>
Next, using JavaScript, the page can drawing shapes by the 2d
context, which is widely supported across browsers. Then it can specify the color and dimensions of the rectangle, which is drawn starting at coordinates (30, 50) with a width of 80 pixels and a height of 100 pixels:
let canvas = document.getElementById("myCanvas");
let ctx = canvas.getContext("2d");
ctx.fillStyle = "#FF0000"; // Red color
ctx.fillRect(30, 50, 80, 100); // Draws a rectangle
Drawing Text on Canvas
Despite adjustment of the font colors through CSS like override-colors
, what is actually stored in computer is the glyph's code point and its associated font data. If anyone copies this colored Emoji to another website without applying the CSS, the customization color will be lost, and the text will typically revert to its default palette.
Because the color information is tied to the specific CSS, to preserve the customized colors of a color font, especially when sharing or texting the Emoji to other environments, converting the Emoji into an image is an effective approach. This method ensures a way to share and use the styled text without any CSS relying.
For example, the <canvas>
element can also be used to render text. Suppose the page has a <div>
element with Emoji:
<div id="dolphin" style="font-family: Noto Color Emoji; font-size: 10em">
🐬
</div>
To replicate this text as an image, the <canvas>
needs to match the size of the <div>
based on the dimensions of the <div>
:
canvas.width = document.getElementById("dolphin").clientWidth;
canvas.height = document.getElementById("dolphin").clientHeight;
Then set the font size and family to match the original text and draw the Emoji on the canvas:
ctx.font = "10em Noto Color Emoji";
ctx.fillText("🐬", canvas.width / 2, canvas.height / 2);
Enhancing Image Quality
If the resulting image appears blurred, it’s likely because the canvas was rendered at a 1:1 scale. To improve quality, we can try to increase the canvas size proportionally by setting a scaleProp
parameter:
// For example, 10x the image
const scaleProp = 10;
canvas.width = document.getElementById("dolphin").clientWidth * scaleProp;
canvas.height = document.getElementById("dolphin").clientHeight * scaleProp;
ctx.scale(scaleProp, scaleProp);
This ensures the high quality and clarity of image.
Saving the Image
Finally, the user can save the canvas content as an image file by converting the canvas to a data URL and triggering a download with a simple script:
const dataURL = canvas.toDataURL("image/png");
const downloadLink = document.createElement("a");
downloadLink.href = dataURL;
// For example, to save it as a PNG file
downloadLink.download = "my_emoji.png";
// Simulates a click to trigger download
downloadLink.click();
Whether we are working with color fonts or simple graphics, <canvas>
offers a flexible and powerful way to handle the visual contents.
UI Design
Just draw some draft!
Desktop UI
For the desktop, I draft a three-column layout with different sections:
Yellow Area
- Emoji Picker: Allow users to select Emojis. It’s essentially the interface for Emoji selection.Green Area
- Customized Emoji: Display the Emojis after user has been customized or colored. It could either be a text<div>
or an<canvas>
area .Brown Area
- Reference Emoji: Show the original Emoji. This helps users compare the original and modified versions. Like the Customized Emoji, this can be a text or image format.Orange Area
- Buttons: Contain functional buttons such as Random Emoji Select, Random Color Select , Reset Emoji Palette, Save Image, and Share.Pink Area
- Color Pickers: Pick and apply colors to Emojis. When a new color is chosen, it triggers theoverride-colors
style modification.Gray Area
- Footer: Reserve for additional information or links.
Mobile UI
For the mobile, I draft another single-column layout for mobile-client experience.
The primary focus is on the Customized Emoji, which occupies the main view area. However, unlike the desktop, I remove the Reference Emoji in mobile UI to simplify the interface.
Instead of being fixed layout, the Emoji picker is opened via an Open Button. Clicking Open Button will overlay the Emoji picker on top of the current view. Once an Emoji is selected or the Open Button is clicked again, the picker will automatically close.
Usage
Extend Emoji & For fun
Cross-cultural Design
Accessibility
For some people with Color weakness or color blindness, it may be challenging to correctly identify certain emojis that are too similar in color or overly vibrant.
For instance, 🇮🇹
Italy and 🇮🇪
Ireland; 🇷🇴
Romania and 🇹🇩
Chad; 🇱🇻
Latvia and 🇦🇹
Austria; 🇲🇨
Monaco and 🇮🇩
Indonesia. However, most current emoji designs do not take them into consideration. I hope that Emoji Salon can provide easily distinguishable emojis for such individuals.
See More about accessibility:
By implementing these tools and reading specs, I gain a deeper understanding of font structures and typographic experiences.