CSS font-palette and @font-palette-values

A color font carries its own list of colors. font-palette picks which list the text uses, and @font-palette-values lets you replace single entries with colors of your own.

The CSS font-palette property chooses which color palette a color font draws with. Pair it with an @font-palette-values rule and you can replace individual colors in the font, while the shapes and shading stay as the font maker drew them.

The color property does not reach these glyphs. Windows ships a color font that works for this: Segoe UI Emoji. The example below turns its green square purple by replacing four palette entries.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>font-palette basics</title>
<style>
  /* 1. Give the installed color font a name of your own */
  @font-face {
    font-family: 'Emoji';
    src: local('Segoe UI Emoji');
  }

  /* 2. Describe a palette: which color slots to replace, and with what */
  @font-palette-values --purple {
    font-family: 'Emoji';
    override-colors:
      4751 #5b21b6,   /* was dark green  */
      6419 #7c3aed,
      12013 #8b5cf6,
      17867 #c4b5fd;  /* was light green */
  }

  body { margin: 0; padding: 18px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .row { display: flex; gap: 14px; flex-wrap: wrap; }
  figure { margin: 0; flex: 1 1 140px; background: #fff; border-radius: 12px; padding: 14px; text-align: center; }
  .emoji { font-family: 'Emoji'; font-size: 88px; line-height: 1.1; }
  .recolored { font-palette: --purple; }  /* 3. Use the palette */
  figcaption { font-size: 14px; }
  code { font: 13px ui-monospace, Consolas, monospace; background: #eef1f5; padding: 1px 4px; border-radius: 4px; }
  .note { font-size: 13px; color: #5b6472; margin: 12px 0 0; }
</style>
</head>
<body>
<div class="row">
  <figure><div class="emoji">&#x1F7E9;</div><figcaption>Default palette</figcaption></figure>
  <figure><div class="emoji recolored">&#x1F7E9;</div><figcaption><code>font-palette: --purple</code></figcaption></figure>
</div>
<p class="note">Uses the Segoe UI Emoji color font that ships with Windows. On other systems both squares keep their own emoji colors.</p>
</body>
</html>
The same emoji, drawn with the font palette and with four entries replaced. Needs Windows, where Segoe UI Emoji is installed.

On a Mac or a phone, the emoji comes from a different font and both squares stay green. That is expected: the rule names one font, so it changes only that font.

How font-palette works: three pieces

A palette change always has the same three parts. The font gets a family name, a palette is described for that family, and an element uses the palette by name.

Name the font, describe the palette, then use it.
Name the font, describe the palette, then use it.
@font-face {
  font-family: 'Emoji';
  src: local('Segoe UI Emoji');   /* the font already on the computer */
}

@font-palette-values --purple {
  font-family: 'Emoji';           /* must match the family the text uses */
  override-colors: 4751 #5b21b6, 6419 #7c3aed, 12013 #8b5cf6, 17867 #c4b5fd;
}

.icon {
  font-family: 'Emoji';
  font-palette: --purple;
}

The palette name must start with two dashes, like a custom property. src: local() uses a font installed on the viewer's device, so nothing is downloaded. For fonts you ship yourself, see web fonts.

Palettes are numbered color slots

A COLR color font does not store colors inside each glyph. Each shape points at an entry in a table called CPAL, and the entry holds the color. override-colors swaps the color in an entry, and every shape that points at it changes.

The green square uses four palette entries. override-colors gives the same entries new colors.
The green square uses four palette entries. override-colors gives the same entries new colors.

Segoe UI Emoji on our Windows 11 machine (font version 1.70) has one palette with 65,429 entries. The green square uses 4 of them. Entries you do not list keep the font's own color.

The font-palette property itself takes these values:

Value What it picks Segoe UI Emoji, measured
normal The font's default palette The usual emoji colors
light A palette the font flags for light backgrounds No change: the font has no flagged palette
dark A palette the font flags for dark backgrounds No change, for the same reason
--name A palette you defined with @font-palette-values The listed entries change

Inside @font-palette-values, base-palette chooses which of the font's palettes to start from, by number. With only one palette in this font, base-palette: 1 fell back to the default palette in Chromium, Edge and Firefox.

Finding the index numbers

The numbers come from the font file, not from CSS. This Python script uses the fontTools library to list the entries one emoji draws with. Put the emoji you want to inspect in the glyph = line.

from fontTools.ttLib import TTFont

font = TTFont(r'C:\Windows\Fonts\seguiemj.ttf')
colr = font['COLR'].table
palette = font['CPAL'].palettes[0]
paints = {r.BaseGlyph: r.Paint for r in colr.BaseGlyphList.BaseGlyphPaintRecord}
layers = colr.LayerList.Paint

def slots(paint, found):
    if hasattr(paint, 'PaletteIndex'):
        found.add(paint.PaletteIndex)
    if hasattr(paint, 'ColorLine'):
        found.update(stop.PaletteIndex for stop in paint.ColorLine.ColorStop)
    if paint.Format == 1:  # a list of layers
        for p in layers[paint.FirstLayerIndex:paint.FirstLayerIndex + paint.NumLayers]:
            slots(p, found)
    if paint.Format == 11:  # reuses another glyph's drawing
        slots(paints[paint.Glyph], found)
    for child in ('Paint', 'SourcePaint', 'BackdropPaint'):
        if hasattr(paint, child):
            slots(getattr(paint, child), found)
    return found

glyph = font.getBestCmap()[ord('\U0001F7E9')]  # the emoji to inspect
for i in sorted(slots(paints[glyph], set())):
    c = palette[i]
    print(i, '#%02x%02x%02x' % (c.red, c.green, c.blue))

For the green square it prints 4751 #388964, 6419 #3fb180, 12013 #55d894 and 17867 #6ff4a4, darkest to lightest. Map each one to a new shade of similar lightness and the 3D shading survives.

This font has two drawings per emoji: a newer one (COLR version 1, with gradients) and an older flat one with its own index numbers. Chromium, Edge and Firefox drew the newer one in our test, so the script reads that table.

The numbers are tied to this font version and may change after a Windows update.

Why a system font needs @font-face local()

The palette rule names a family. The obvious choice is the installed name, 'Segoe UI Emoji'. Firefox accepted that, but Chromium and Edge ignored the palette. Declaring the same font through @font-face with src: local() made it work everywhere the feature worked at all.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Naming the font directly vs @font-face local()</title>
<style>
  /* The same four color slots, pointed at two different family names */
  @font-palette-values --direct {
    font-family: 'Segoe UI Emoji';  /* the installed font's own name */
    override-colors: 4751 #c2410c, 6419 #ea580c, 12013 #f97316, 17867 #fdba74;
  }

  @font-face {
    font-family: 'Emoji';
    src: local('Segoe UI Emoji');   /* the same file, under a name you declared */
  }
  @font-palette-values --viaFace {
    font-family: 'Emoji';
    override-colors: 4751 #c2410c, 6419 #ea580c, 12013 #f97316, 17867 #fdba74;
  }

  body { margin: 0; padding: 18px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .row { display: flex; gap: 14px; flex-wrap: wrap; }
  figure { margin: 0; flex: 1 1 150px; background: #fff; border-radius: 12px; padding: 14px; text-align: center; }
  .emoji { font-size: 80px; line-height: 1.1; }
  .a { font-family: 'Segoe UI Emoji'; font-palette: --direct; }
  .b { font-family: 'Emoji'; font-palette: --viaFace; }
  figcaption { font-size: 14px; line-height: 1.45; }
  code { font: 12.5px ui-monospace, Consolas, monospace; background: #eef1f5; padding: 1px 4px; border-radius: 4px; }
  .note { font-size: 13px; color: #5b6472; margin: 12px 0 0; }
</style>
</head>
<body>
<div class="row">
  <figure><div class="emoji a">&#x1F7E9;</div><figcaption>Named directly<br><code>'Segoe UI Emoji'</code></figcaption></figure>
  <figure><div class="emoji b">&#x1F7E9;</div><figcaption>Through <code>@font-face</code><br><code>local('Segoe UI Emoji')</code></figcaption></figure>
</div>
<p class="note">Orange means the palette applied. Open this in more than one browser on Windows to compare.</p>
</body>
</html>
Left: the palette names the installed font directly. Right: the same font, declared with @font-face. Orange means the palette applied.

We ran each demo in Playwright's Chromium, Firefox and WebKit builds and in installed Microsoft Edge, on Windows 11:

Engine (Windows 11) Font named directly Through @font-face local()
Chromium Not applied Applied
Microsoft Edge Not applied Applied
Firefox Applied Applied
WebKit (Playwright build) Not applied Not applied

WebKit drew the emoji in color and reported font-palette as supported in CSS.supports(), yet kept the original colors. So a feature check does not prove the palette renders.

Safari on a Mac was not tested, and Segoe UI Emoji is not installed there. The same local() workaround helped with variable font axes on Windows.

Overrides reach every glyph that shares an index

Palette entries are shared. In Segoe UI Emoji, the blue square badges such as NEW, FREE, UP! and the P parking sign use the same four blue entries. A palette that turns one of them green turns all of them green, wherever font-palette applies.

The palette is inherited. Set on a wide parent, it also recolors emoji in running text that share the same entries.
The palette is inherited. Set on a wide parent, it also recolors emoji in running text that share the same entries.

font-palette is inherited, so setting it on body reaches every emoji on the page. Put it on the element that holds the icons you mean to change, and leave running text alone.

A finished example: emoji badges in theme colors

Five badges share the same four blue entries, so three short palettes give three themes. A button sets data-theme on the body, and one CSS rule per theme picks the palette.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Emoji badges in theme colors</title>
<style>
  @font-face { font-family: 'Emoji'; src: local('Segoe UI Emoji'); }

  /* These blue badges share four color slots, dark to light */
  @font-palette-values --green {
    font-family: 'Emoji';
    override-colors: 6372 #166534, 10958 #15803d, 13390 #22c55e, 20467 #86efac;
  }
  @font-palette-values --orange {
    font-family: 'Emoji';
    override-colors: 6372 #9a3412, 10958 #c2410c, 13390 #f97316, 20467 #fdba74;
  }
  @font-palette-values --grey {
    font-family: 'Emoji';
    override-colors: 6372 #374151, 10958 #4b5563, 13390 #6b7280, 20467 #9ca3af;
  }

  body { margin: 0; padding: 18px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .badges { font-family: 'Emoji'; font-size: 44px; line-height: 1.2; background: #fff; border-radius: 12px; padding: 12px 14px; letter-spacing: 6px; }
  /* font-palette is inherited, so setting it on the box reaches every badge */
  [data-theme="green"]  .badges { font-palette: --green; }
  [data-theme="orange"] .badges { font-palette: --orange; }
  [data-theme="grey"]   .badges { font-palette: --grey; }

  .themes { display: flex; gap: 8px; flex-wrap: wrap; margin: 14px 0 8px; }
  button { font: 600 14px system-ui, sans-serif; padding: 8px 14px; border-radius: 99px; border: 1px solid #cfd4dc; background: #fff; cursor: pointer; }
  button[aria-pressed="true"] { background: #1d2330; color: #fff; border-color: #1d2330; }
  .out { font: 13px ui-monospace, Consolas, monospace; color: #374151; }
</style>
</head>
<body data-theme="blue">
<div class="badges" id="badges">&#x1F195;&#x1F193;&#x1F192;&#x1F199;&#x1F197;</div>

<div class="themes" id="themes">
  <button data-theme="blue" aria-pressed="true">Blue (font default)</button>
  <button data-theme="green" aria-pressed="false">Green</button>
  <button data-theme="orange" aria-pressed="false">Orange</button>
  <button data-theme="grey" aria-pressed="false">Grey</button>
</div>
<div class="out" id="out">font-palette: normal</div>

<script>
  const themes = document.getElementById('themes');
  const out = document.getElementById('out');

  themes.addEventListener('click', (e) => {
    const btn = e.target.closest('button');
    if (!btn) return;
    document.body.dataset.theme = btn.dataset.theme;  // CSS picks the palette
    themes.querySelectorAll('button').forEach((b) => b.setAttribute('aria-pressed', b === btn));
    // show the computed value the badges ended up with
    out.textContent = 'font-palette: ' + getComputedStyle(document.getElementById('badges')).fontPalette;
  });
</script>
</body>
</html>
Pick a theme. The badges keep their lettering and shading, and only the four shared entries change.
  • One palette per theme: each @font-palette-values rule lists the same four indices with different colors.
  • CSS does the switching: [data-theme="green"] .badges sets font-palette: --green. The script only changes the attribute.
  • Reading it back: getComputedStyle(el).fontPalette returns normal or the palette name, which the demo prints under the buttons.

To follow the system dark mode instead of buttons, set font-palette inside a prefers-color-scheme media query. Dark mode in CSS covers that query. For one-color text and icons, font color is still the color property.

When it does not work

What you see Cause Fix
No change in Chrome or Edge, works in Firefox The palette names the installed font directly Declare it with @font-face and src: local(), and use that family
No change in any browser The family in @font-palette-values differs from the text's font-family Use the same family name in both
No change on a Mac or phone The emoji comes from a different font there Expected. The palette only applies to the named font
Only part of the glyph changed Other shapes use entries you did not list List every index the glyph uses
Emoji elsewhere on the page changed Shared entries, and font-palette is inherited Set font-palette on a narrower element
light or dark look the same as normal The font has no palettes flagged for light or dark Define your own palette with override-colors
Worked before, now wrong colors A font update renumbered the entries Run the index script again

A palette change is a visual result, and it depends on the fonts of the device that shows it. A shared page lets people on Windows see the recolored emoji in their own browser, and lets you compare engines by opening one address.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can switch the themes themselves. If you change the palettes later, the same link shows the new version.

Questions people ask

Can font-palette recolor any emoji?

Only glyphs from a color font that has a color palette table (CPAL), such as fonts in the COLR format. A color font stored as bitmap images has no palette to change, and an ordinary one-color font is recolored with the color property instead.

Why does my @font-palette-values rule do nothing?

Check three things. The font-family inside the rule must match the family the text uses. The index numbers must exist in that font. And in our Windows test, Chrome and Edge ignored the palette when the installed font was named directly; declaring it with @font-face and src: local() fixed it.

Do font-palette: light and dark follow the dark mode setting?

No. They pick a palette that the font maker flagged for light or dark backgrounds. To switch with the system setting, set font-palette inside a prefers-color-scheme media query. A font with no flagged palettes, such as Segoe UI Emoji in our test, looks the same with either value.

Where do the index numbers in override-colors come from?

From the font file. Each shape in a COLR glyph points at a numbered entry in the CPAL palette. A short fontTools script, shown in this guide, lists the entries a given character uses. The numbers belong to that version of the font and can change when the font is updated.

Is font-palette inherited?

Yes. Setting it on a parent reaches every color glyph inside, which is handy for a group of icons. It also means emoji in running text inside that parent change when they share an index with the ones you meant.

Keep reading