# Emoji and fonts in Puppeteer screenshots: Docker, Linux and web fonts

> Why emoji render as boxes and fonts look wrong in Puppeteer on Linux and Docker: what the official image lacks, the packages that fix it, web fonts.

Published 2026-11-06 · https://shareglint.com/en/guides/puppeteer-emoji-and-fonts

Emoji that render as empty boxes, Chinese text as squares, a brand font replaced by Times — in Puppeteer these are almost always **fonts missing on the machine**, not bugs in your page. Chrome draws text with the fonts the operating system has, and Linux servers and Docker images ship few. This page shows what is missing in a typical setup, which packages fix it, and how to make sure web fonts have loaded before the capture. The tests were run on 2026-11-06 with Puppeteer 25.7.0 in the official `ghcr.io/puppeteer/puppeteer` image.

## What the official Puppeteer image renders

We rendered one line each of emoji, Japanese, Chinese and Korean, Polish and Arabic in the official image, unchanged:

- **CJK, Arabic and Polish rendered correctly.** The image installs fonts for them (it lists 125 fonts; six cover Japanese).
- **Emoji did not.** `fc-list` found **no emoji font**. 😀 came out as a black-and-white outline from a text font, 🎉, 👍🏽 and 🇵🇱 as empty boxes, and a pixel count found no coloured pixels in the emoji line.

After adding two Debian packages to the image, the same page rendered all four emoji in colour (about 2,250 strongly coloured pixels in that line) and the image listed 156 fonts:

```dockerfile
FROM ghcr.io/puppeteer/puppeteer:25.7.0
USER root
RUN apt-get update \
 && apt-get install -y --no-install-recommends fonts-noto-color-emoji fonts-noto-cjk \
 && rm -rf /var/lib/apt/lists/*
USER pptruser
```

If you build your own image from `node:*-slim` or another minimal base, assume it has even fewer fonts and install at least:

| Need | Debian / Ubuntu package |
|---|---|
| Colour emoji | `fonts-noto-color-emoji` |
| Chinese, Japanese, Korean | `fonts-noto-cjk` |
| Latin, Greek, Cyrillic with common metrics | `fonts-liberation` (metric-compatible with Arial, Times New Roman, Courier New) |
| Many other scripts | `fonts-noto-core` (large) |

The emoji package installs Google's Noto Color Emoji font ([Debian package](https://packages.debian.org/bookworm/fonts-noto-color-emoji)). Alpine uses different names (`font-noto-emoji`, `font-noto-cjk`). Install fonts at image build time; if you add them to a running container, restart Chrome so it picks them up.

## Check what a container has

Before changing anything, ask the container itself. Chrome on Linux finds fonts through fontconfig, so its command-line tools show what Chrome can use:

```bash
fc-list | wc -l                    # how many fonts are installed
fc-list | grep -i emoji            # any emoji font? (none in the official Puppeteer image)
fc-list :lang=ja family            # fonts that cover Japanese
fc-match "Noto Color Emoji"        # what a request for this family resolves to
```

If `fc-match` answers with a different family than the one you asked for, the font is not installed and Chrome will fall back as well. Run these in the same image that runs Puppeteer — a font on your build machine or your laptop does not count.

## How Chrome picks a font for a character

For each character Chrome tries the fonts named in your CSS (`font-family`), then falls back to system fonts that contain the character, found through fontconfig on Linux. If no installed font has a glyph, you get an empty box, often called "tofu". That is why the fix is a package, not CSS: `font-family: "Noto Color Emoji"` does nothing on a machine without that font.

Two consequences:

- **The same page looks different on your laptop and in production.** macOS and Windows ship emoji and CJK fonts; your container may not. Render a test page with emoji and CJK on the production image once.
- **Text width changes with the fallback font**, so a layout that fits on your machine can wrap differently in the container. Install the fonts your design assumes, or load them as web fonts.

## Web fonts: wait for them

Fonts loaded with `@font-face` arrive over the network after the HTML. If the screenshot runs first, it shows the fallback font — or no text, during the font's block period. We served a font with a 1.5-second delay and measured the text's width:

| `waitUntil` | Font status at capture | Width |
|---|---|---|
| `domcontentloaded` | loading | 361 px (fallback font) |
| `load` | loaded | 260 px |
| `networkidle0` | loaded | 260 px |

After `await page.evaluate(() => document.fonts.ready)`, every variant had the right font. `font-display: swap` made no difference to the result. `load` waited for the font here because the text using it was in the initial HTML; text added later by JavaScript, or fonts requested by a script, can arrive after `load`. So whatever `waitUntil` you use, add:

```javascript
await page.goto(url, { waitUntil: 'networkidle2' });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'shot.png' });
```

`document.fonts.ready` resolves when the fonts the page has requested so far are loaded ([MDN](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready)).

## Fonts for OG images and generated cards

If you render your own HTML (cards, certificates, OG images) rather than other people's pages, you control the fonts completely:

- **Embed them as data URLs** in the HTML or serve them locally, so the render never waits on a font CDN.
- **Subset them** to the characters you use, to keep the page small.
- **Name a fallback with the same metrics** (for example Liberation Sans for Arial) so a missing font does not reflow the layout.
- **Add an emoji font to the stack** explicitly, after your text font: `font-family: "Inter", "Noto Color Emoji", sans-serif`.

## Or use a renderer that has them

Shareglint renders templates and screenshots in a Chrome that already has colour emoji and CJK fonts installed; the list of families for templates is at `GET /v1/fonts` (you cannot upload your own font file there — templates use the installed families). For screenshots of other sites, the site's own web fonts load as in any browser. Details on [the screenshot API](https://shareglint.com/en/screenshot-api) and [HTML to image](https://shareglint.com/en/html-to-image) pages.

## FAQ

### Why are emoji boxes in my Puppeteer screenshot?

Chrome draws emoji with an emoji font installed on the machine, and Linux containers often have none. In the official Puppeteer Docker image we found no emoji font: most emoji came out as empty boxes. Installing fonts-noto-color-emoji fixed it.

### Which packages do I install for emoji and Asian text in Docker?

On Debian or Ubuntu: fonts-noto-color-emoji for colour emoji and fonts-noto-cjk for Chinese, Japanese and Korean. The official Puppeteer image already renders CJK, Arabic and Polish, but not colour emoji.

### Why does my web font not show up in the screenshot?

The screenshot was taken before the font arrived. With waitUntil domcontentloaded the text used the fallback font in our test; with load the font was ready. Wait for document.fonts.ready before capturing, whatever waitUntil you use.

### Can I use emoji in a Puppeteer PDF too?

Yes, with the same font installed. In our test Chrome embedded the colour emoji in the PDF as Type3 font glyphs, and they printed in colour; without the font they are boxes there too.
