Table of contents

How do I configure custom fonts and CSS in Headless Chromium?

Configuring custom fonts and CSS in Headless Chromium is essential for accurate web scraping, PDF generation, and automated testing scenarios where visual consistency and custom styling are crucial. This comprehensive guide covers various methods to inject custom fonts and CSS styles into your Headless Chromium instances.

Understanding Font and CSS Configuration in Headless Chromium

When running Chromium in headless mode, the browser operates without a GUI, which can affect font rendering and CSS application. Custom font and CSS configuration becomes necessary when:

  • Generating PDFs with specific typography
  • Scraping websites that require custom styling for proper rendering
  • Testing responsive designs with custom fonts
  • Creating screenshots with consistent visual appearance
  • Handling web applications that rely on specific font metrics

Method 1: Using Puppeteer for Font and CSS Injection

Puppeteer provides the most straightforward approach to configure custom fonts and CSS in Headless Chromium.

Installing Custom Fonts

const puppeteer = require('puppeteer');

async function setupCustomFonts() {
  const browser = await puppeteer.launch({
    headless: true,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--font-render-hinting=none',
      '--disable-font-subpixel-positioning'
    ]
  });

  const page = await browser.newPage();

  // Method 1: Load web fonts via CSS
  await page.addStyleTag({
    content: `
      @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap');

      body {
        font-family: 'Roboto', Arial, sans-serif;
      }
    `
  });

  // Method 2: Load local font files
  await page.addStyleTag({
    content: `
      @font-face {
        font-family: 'CustomFont';
        src: url('file:///path/to/your/font.woff2') format('woff2'),
             url('file:///path/to/your/font.woff') format('woff');
        font-weight: normal;
        font-style: normal;
      }

      .custom-text {
        font-family: 'CustomFont', fallback-font, sans-serif;
      }
    `
  });

  await page.goto('https://example.com');
  await browser.close();
}

Injecting Custom CSS Styles

async function injectCustomCSS() {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();

  // Method 1: Add CSS from string
  await page.addStyleTag({
    content: `
      body {
        background-color: #f0f0f0;
        margin: 0;
        padding: 20px;
      }

      .highlight {
        background-color: yellow;
        padding: 5px;
        border-radius: 3px;
      }

      h1, h2, h3 {
        color: #333;
        font-weight: 600;
      }
    `
  });

  // Method 2: Load CSS from external file
  await page.addStyleTag({
    path: './custom-styles.css'
  });

  // Method 3: Load CSS from URL
  await page.addStyleTag({
    url: 'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
  });

  await page.goto('https://example.com');

  // Wait for fonts to load before taking screenshot
  await page.evaluateHandle('document.fonts.ready');

  await page.screenshot({ path: 'styled-page.png' });
  await browser.close();
}

Method 2: Using Chrome DevTools Protocol (CDP)

For more advanced control, you can use the Chrome DevTools Protocol directly:

const puppeteer = require('puppeteer');

async function configureFontsViaCDP() {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();

  // Get CDP session
  const client = await page.target().createCDPSession();

  // Enable CSS domain
  await client.send('CSS.enable');

  // Add custom stylesheet
  const { styleSheetId } = await client.send('CSS.createStyleSheet', {
    frameId: page.mainFrame()._id
  });

  await client.send('CSS.setStyleSheetText', {
    styleSheetId,
    text: `
      @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');

      * {
        font-family: 'Inter', system-ui, sans-serif;
      }

      body {
        line-height: 1.6;
        color: #2d3748;
      }
    `
  });

  await page.goto('https://example.com');
  await browser.close();
}

Method 3: System-Level Font Installation

For consistent font rendering across multiple Chromium instances, install fonts at the system level:

Linux (Docker/Server environments)

# Dockerfile example
FROM node:16-slim

# Install fonts
RUN apt-get update && apt-get install -y \
    fonts-liberation \
    fonts-dejavu-core \
    fontconfig \
    && rm -rf /var/lib/apt/lists/*

# Copy custom fonts
COPY fonts/ /usr/share/fonts/truetype/custom/

# Update font cache
RUN fc-cache -f -v

# Install your application
COPY package*.json ./
RUN npm install
COPY . .

CMD ["node", "app.js"]

Installing fonts programmatically

# Install common web fonts on Ubuntu/Debian
sudo apt-get update
sudo apt-get install fonts-liberation fonts-dejavu-core fonts-noto

# Install custom fonts
sudo mkdir -p /usr/share/fonts/truetype/custom
sudo cp /path/to/your/fonts/*.ttf /usr/share/fonts/truetype/custom/
sudo fc-cache -f -v

Method 4: Advanced Font Configuration with Puppeteer

const puppeteer = require('puppeteer');
const fs = require('fs');

class FontManager {
  constructor() {
    this.loadedFonts = new Set();
  }

  async setupBrowser() {
    this.browser = await puppeteer.launch({
      headless: true,
      args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-dev-shm-usage',
        '--disable-gpu',
        '--font-render-hinting=none'
      ]
    });
  }

  async loadFontFromFile(fontPath, fontFamily) {
    if (this.loadedFonts.has(fontFamily)) return;

    const fontBuffer = fs.readFileSync(fontPath);
    const fontBase64 = fontBuffer.toString('base64');
    const fontExtension = fontPath.split('.').pop().toLowerCase();

    let fontFormat;
    switch (fontExtension) {
      case 'woff2': fontFormat = 'woff2'; break;
      case 'woff': fontFormat = 'woff'; break;
      case 'ttf': fontFormat = 'truetype'; break;
      case 'otf': fontFormat = 'opentype'; break;
      default: throw new Error(`Unsupported font format: ${fontExtension}`);
    }

    const fontCSS = `
      @font-face {
        font-family: '${fontFamily}';
        src: url('data:font/${fontFormat};base64,${fontBase64}') format('${fontFormat}');
        font-display: swap;
      }
    `;

    this.loadedFonts.add(fontFamily);
    return fontCSS;
  }

  async createStyledPage(customCSS = '') {
    const page = await this.browser.newPage();

    // Load all font CSS
    const fontCSS = Array.from(this.loadedFonts).map(font => 
      `body { font-family: '${font}', Arial, sans-serif; }`
    ).join('\n');

    await page.addStyleTag({
      content: fontCSS + customCSS
    });

    return page;
  }

  async close() {
    if (this.browser) {
      await this.browser.close();
    }
  }
}

// Usage example
async function useCustomFontManager() {
  const fontManager = new FontManager();
  await fontManager.setupBrowser();

  // Load custom fonts
  const robotoCSS = await fontManager.loadFontFromFile('./fonts/Roboto-Regular.ttf', 'Roboto');
  const openSansCSS = await fontManager.loadFontFromFile('./fonts/OpenSans-Regular.woff2', 'Open Sans');

  const page = await fontManager.createStyledPage(`
    ${robotoCSS}
    ${openSansCSS}

    h1 { font-family: 'Roboto', sans-serif; }
    p { font-family: 'Open Sans', sans-serif; }

    .custom-styling {
      background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 20px;
      border-radius: 10px;
    }
  `);

  await page.goto('https://example.com');

  // Wait for fonts to load
  await page.evaluate(() => document.fonts.ready);

  await page.screenshot({ path: 'custom-styled-page.png' });
  await fontManager.close();
}

Python Implementation with Selenium

You can also configure fonts and CSS using Python with Selenium WebDriver:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
import base64

def setup_headless_chrome_with_fonts():
    chrome_options = Options()
    chrome_options.add_argument('--headless')
    chrome_options.add_argument('--no-sandbox')
    chrome_options.add_argument('--disable-dev-shm-usage')
    chrome_options.add_argument('--font-render-hinting=none')
    chrome_options.add_argument('--disable-font-subpixel-positioning')

    driver = webdriver.Chrome(options=chrome_options)
    return driver

def inject_custom_css_and_fonts(driver, css_content):
    """Inject custom CSS into the page"""
    script = f"""
    var style = document.createElement('style');
    style.textContent = `{css_content}`;
    document.head.appendChild(style);
    """
    driver.execute_script(script)

def load_font_from_file(font_path, font_family):
    """Load font from file and create CSS"""
    with open(font_path, 'rb') as font_file:
        font_data = base64.b64encode(font_file.read()).decode()

    font_extension = font_path.split('.')[-1].lower()
    format_mapping = {
        'woff2': 'woff2',
        'woff': 'woff',
        'ttf': 'truetype',
        'otf': 'opentype'
    }

    font_format = format_mapping.get(font_extension, 'truetype')

    css = f"""
    @font-face {{
        font-family: '{font_family}';
        src: url('data:font/{font_format};base64,{font_data}') format('{font_format}');
        font-display: swap;
    }}
    """
    return css

# Usage example
driver = setup_headless_chrome_with_fonts()

# Load custom font
font_css = load_font_from_file('./fonts/Roboto-Regular.woff2', 'Roboto')

# Combine with custom styles
custom_css = font_css + """
body {
    font-family: 'Roboto', Arial, sans-serif;
    background-color: #f8f9fa;
    margin: 20px;
}

.highlighted {
    background-color: #fff3cd;
    border: 1px solid #ffeaa7;
    padding: 10px;
    border-radius: 4px;
}
"""

driver.get('https://example.com')
inject_custom_css_and_fonts(driver, custom_css)

# Wait for fonts to load
driver.execute_script("return document.fonts.ready;")

driver.save_screenshot('styled_page.png')
driver.quit()

Handling Font Loading and Timing

Proper font loading is crucial for consistent rendering. Here's how to ensure fonts are fully loaded:

async function waitForFontsToLoad(page) {
  // Method 1: Wait for document.fonts.ready
  await page.evaluate(() => document.fonts.ready);

  // Method 2: Wait for specific font to load
  await page.evaluate((fontFamily) => {
    return document.fonts.load(`16px "${fontFamily}"`);
  }, 'Roboto');

  // Method 3: Wait with timeout
  await page.evaluateHandle(() => {
    return Promise.race([
      document.fonts.ready,
      new Promise(resolve => setTimeout(resolve, 5000))
    ]);
  });
}

Best Practices and Troubleshooting

Font Loading Best Practices

  1. Use font-display: swap for better performance
  2. Preload critical fonts using <link rel="preload">
  3. Provide fallback fonts for better compatibility
  4. Wait for fonts to load before taking screenshots or generating PDFs

Common Issues and Solutions

Issue: Fonts not rendering correctly in headless mode

// Solution: Add specific Chrome flags
const browser = await puppeteer.launch({
  headless: true,
  args: [
    '--font-render-hinting=none',
    '--disable-font-subpixel-positioning',
    '--disable-gpu-sandbox'
  ]
});

Issue: System fonts not available in Docker

# Solution: Install font packages
RUN apt-get update && apt-get install -y \
    fonts-liberation \
    fonts-dejavu-core \
    fonts-noto-cjk \
    && fc-cache -f -v

Issue: Fonts loading slowly or inconsistently

// Solution: Use proper font loading strategy
await page.addStyleTag({
  content: `
    @font-face {
      font-family: 'CustomFont';
      src: url('path/to/font.woff2') format('woff2');
      font-display: block; /* Block rendering until font loads */
    }
  `
});

// Wait for all fonts to be ready
await page.evaluateHandle('document.fonts.ready');

Performance Considerations

When working with custom fonts and CSS in Headless Chromium, consider these performance optimizations:

  • Cache font files locally when possible to avoid network requests
  • Use efficient font formats (WOFF2 over TTF) for smaller file sizes
  • Minimize CSS injection operations by batching style additions
  • Subset fonts to include only necessary characters
  • Use font-display: swap for better perceived performance
  • Handle browser sessions efficiently to reuse font configurations across multiple pages

Advanced CSS Techniques

CSS Custom Properties for Dynamic Styling

await page.addStyleTag({
  content: `
    :root {
      --primary-font: 'Roboto', system-ui, sans-serif;
      --secondary-font: 'Open Sans', Arial, sans-serif;
      --accent-color: #667eea;
      --text-color: #2d3748;
    }

    body {
      font-family: var(--primary-font);
      color: var(--text-color);
    }

    h1, h2, h3 {
      font-family: var(--secondary-font);
      color: var(--accent-color);
    }
  `
});

Responsive Typography

await page.addStyleTag({
  content: `
    html {
      font-size: 16px;
    }

    @media (max-width: 768px) {
      html {
        font-size: 14px;
      }
    }

    body {
      font-size: 1rem;
      line-height: 1.6;
    }

    h1 { font-size: 2.5rem; }
    h2 { font-size: 2rem; }
    h3 { font-size: 1.5rem; }
  `
});

Integration with WebScraping.AI

When using WebScraping.AI for projects requiring custom styling, you can combine these techniques with our API. For complex scenarios involving dynamic content, consider how to handle AJAX requests using Puppeteer alongside your font and CSS configurations.

For scenarios where you need to wait for content to load before applying styles, you might want to learn about navigating to different pages using Puppeteer to ensure proper timing and execution order.

Testing Font and CSS Configuration

async function testFontConfiguration() {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();

  // Inject test CSS
  await page.addStyleTag({
    content: `
      @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');

      .test-element {
        font-family: 'Inter', sans-serif;
        font-size: 24px;
        font-weight: 600;
        color: #333;
        padding: 20px;
        background: #f0f0f0;
      }
    `
  });

  // Create test content
  await page.setContent(`
    <html>
      <body>
        <div class="test-element">Test Font Rendering</div>
      </body>
    </html>
  `);

  // Wait for fonts to load
  await page.evaluate(() => document.fonts.ready);

  // Verify font is applied
  const fontFamily = await page.evaluate(() => {
    const element = document.querySelector('.test-element');
    return window.getComputedStyle(element).fontFamily;
  });

  console.log('Applied font family:', fontFamily);

  await browser.close();
}

Conclusion

Configuring custom fonts and CSS in Headless Chromium requires understanding multiple approaches, from simple Puppeteer methods to advanced CDP usage and system-level font installation. Choose the method that best fits your use case, whether you're generating PDFs, taking screenshots, or scraping content that requires specific visual styling.

Key takeaways: - Use Puppeteer's addStyleTag() method for simple font and CSS injection - Implement system-level font installation for consistent rendering across instances - Always wait for fonts to load before capturing visual content - Consider performance implications when loading custom fonts - Test your font configuration across different environments

By following these practices and examples, you'll be able to create reliable, visually consistent Headless Chromium applications that render fonts and styles exactly as intended.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon