Fix Emoji Rendering in HTML to PDF with Headless Chrome
Diagnose missing or monochrome emoji in headless Chrome PDFs by checking Linux font fallback, print CSS, and the PDF viewer.
When emoji are missing from an HTML-to-PDF export, first check whether the environment running headless Chrome can discover a font with the required emoji glyphs. Then compare the page’s print styles with its screen styles: Puppeteer’s page.pdf() uses print media by default, so the PDF can differ from what you see in a browser tab. A monochrome emoji, an absent glyph, and an emoji that disappears only in print can have different causes; there is no single fix for every runtime.
This guide uses Puppeteer examples, but the key checks also apply when another tool drives headless Chrome. Reproduce the exact emoji and deployment environment before changing fonts or styles.
1. Reduce the problem to a reproducible page
Start with the smallest HTML page that still shows the problem. Put the failing emoji and a known-good emoji in otherwise identical elements. Use the same HTML, CSS, browser build, container or CI image, and PDF viewer as the production workflow.
<!doctype html>
<meta charset="utf-8">
<style>
.emoji { font-family: "Noto Color Emoji", sans-serif; font-size: 32px; }
@media print {
.emoji { color: inherit; }
}
</style>
<p class="emoji">Known-good: 😀</p>
<p class="emoji">Problem: 👩💻</p>
Save the source file as UTF-8 and serve or load it the same way as the real page. Compare the screen rendering and PDF output. If only one emoji or sequence fails, preserve the exact characters: joined sequences and variation selectors can affect which glyphs are needed.
2. Check the font in the Chrome runtime
A font installed on your workstation does not automatically exist in a Linux container, WSL environment, or CI runner. The font must be installed in the environment that launches Chrome and discoverable through that environment’s font configuration. Noto Color Emoji is one open source font option; its maintainers note that Linux use may require fontconfig configuration. Noto Emoji project documentation
Check your image or host’s package documentation for how to install the font and refresh the font cache. Then verify that fontconfig can list the intended family from the same runtime. Restart Chrome after changing system fonts so the browser can discover them. Do not assume that installing Chrome’s shared-library dependencies will fix emoji: Puppeteer’s Linux troubleshooting guidance concerns browser launch dependencies, not emoji font fallback. Puppeteer troubleshooting
Use a specific font family in a minimal test, but retain a sensible fallback. A CSS family declaration is a request; it does not install a font or guarantee that every emoji sequence is covered.
3. Compare print CSS with screen CSS
Puppeteer documents that page.pdf() generates a PDF with the print CSS media type. Rules under @media print can hide the element, replace its content, alter its font family, or change its layout. Inspect computed styles and pseudo-elements in both media contexts, and check whether the emoji is inside an element hidden or rewritten for printing. Puppeteer Page.pdf() API
If the intended PDF should use screen styles, emulate screen media before exporting. This changes the CSS media context; it does not add missing fonts or fix unsupported glyphs.
await page.emulateMediaType('screen');
await page.pdf({ path: 'output.pdf', printBackground: true });
When print styling is desired, fix the relevant @media print rule instead. Keep the print and screen font declarations aligned where that is the intended design.
4. Wait for page content and fonts before exporting
If the page uses web fonts or loads content asynchronously, ensure that rendering has completed before calling page.pdf(). The following example waits for navigation, for the document’s fonts to finish loading, and for a known content selector. Adapt the URL, selector, and timeout to your page.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com/report', {
waitUntil: 'networkidle0',
timeout: 60000
});
await page.waitForSelector('.report-content', { timeout: 15000 });
await page.evaluate(() => document.fonts.ready);
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true
});
} finally {
await browser.close();
}
})();
networkidle0 can be unsuitable for pages with persistent network activity. If navigation never reaches that condition, use an appropriate navigation condition and wait for a page-specific selector or readiness signal instead. Waiting for document.fonts.ready covers document font loading; it does not install system fonts or prove that the chosen emoji font supports the glyph.
5. Use a targeted visual fallback when appearance must be fixed
If the target runtime or PDF viewer cannot reliably render the required emoji appearance, use authored SVG or raster artwork for that symbol and test it in the target viewer. This is an engineering fallback to validate, not a universal browser remedy. Preserve accessible text when the symbol carries meaning, for example with descriptive text or an appropriate accessible label.
Inspect the generated PDF in more than one viewer when possible. If output differs, record the viewer and operating system alongside the Chrome and Puppeteer versions; the rendering or display path may be involved after PDF generation.
6. Runnable Puppeteer example
This complete CommonJS script creates a small HTML document, waits for fonts, and writes a PDF. Install Puppeteer in your project using its documented installation instructions, then run the script in the same kind of environment as production. The font declaration is a runtime check, not a font installer.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
const html = `<!doctype html>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; }
.emoji { font-family: "Noto Color Emoji", sans-serif; font-size: 32px; }
@media print { .emoji { font-family: "Noto Color Emoji", sans-serif; } }
</style>
<h1>Emoji PDF check</h1>
<p class="emoji">Known-good: 😀</p>
<p class="emoji">Problem: 👩💻</p>`;
await page.setContent(html, { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
await page.pdf({
path: 'emoji-check.pdf',
format: 'A4',
printBackground: true
});
} finally {
await browser.close();
}
})();
To test screen media instead of print media, add await page.emulateMediaType('screen') immediately before page.pdf(). For a production page, use page.goto() and page-specific readiness checks as in the previous example.
7. Troubleshooting by symptom
| Symptom | Likely area to check | Next step |
|---|---|---|
| Empty space or a tofu box | Glyph coverage, font installation or discovery, content, or CSS | Verify the exact character sequence and confirm the intended font is visible to fontconfig in Chrome’s runtime. |
| Emoji appears in grayscale | Which fallback font rendered it, or color-font support along the rendering path | Check the actual font and compare the PDF in another viewer. Noto documents its color-font format and platform notes, but they do not guarantee a particular runtime result. |
| Emoji appears on screen but not in the PDF | Print media rules or fonts available to the PDF runtime | Inspect @media print styles and compare with screen media. Puppeteer PDF generation uses print media by default. |
| Only some placements or emoji fail | Inherited CSS, pseudo-elements, font coverage, or exact sequence | Reduce the page and compare computed styles for a failing element and a working one. A reported Puppeteer issue describes selective missing emoji, but does not establish a general root cause. |
| Chrome fails to launch | Browser runtime dependencies | Use Puppeteer’s Linux troubleshooting guide to diagnose launch dependencies. Treat this separately from diagnosing emoji fallback. |
| PDF sometimes captures fallback styling | Page or font loading readiness | Wait for the relevant content and font loading, then export. Use a page-specific readiness condition if the site keeps network connections open. |
A Puppeteer issue reports missing emoji selectively in a Linux/WSL PDF workflow; it is a reproduction clue, not proof of a universal Chromium defect or accepted general fix. Puppeteer issue #11120
8. Performance, reliability, and cost
- Performance: Installing fonts in a reusable container image avoids doing setup during each PDF job. Keep the minimal reproduction small while diagnosing so page assets and scripts do not obscure the font and media checks.
- Reliability: Pin and record the Chrome, Puppeteer, OS image, and font versions used by your deployment. Recheck after changing any of them because font availability and rendering support are runtime properties.
- Cost: A self-hosted Puppeteer workflow uses your own compute and maintenance budget. Account for browser processes, font packages, PDF storage, and CI or server execution; the research sources provide no universal per-PDF cost or performance benchmark.
Or skip the browser setup
ScreenshotNeo can return a PDF from one API request, with options for paper size, margins, landscape orientation, and page ranges. It removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. See the ScreenshotNeo website and API documentation for request details.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-d format=pdf \
-o page.pdf
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
FAQ
Does printBackground: true fix missing emoji?
No. It includes background graphics in the PDF; it does not install fonts or change glyph fallback.
Will setting font-family: "Noto Color Emoji" always work?
No. The font must be installed and discoverable in the Chrome runtime, and the target emoji must be covered by the rendering path.
Is this a known universal Chromium bug?
The cited issue records one user report and is closed as not planned. It does not establish a universal defect or root cause.
Which versions should I include in a bug report?
Include the exact emoji string, minimal HTML and CSS, Puppeteer and Chrome versions, operating system or container image, installed emoji fonts, whether screen output works, and the PDF viewer and OS where the PDF was inspected.


