Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Inject the CSS string before invoking the PDF renderer. In Playwright or Puppeteer, add a <style> element with page.addStyleTag({ content: cssString }), then select the intended media type and call page.pdf(). In WeasyPrint, construct a CSS(string=...) object and pass it to HTML.write_pdf(). The order matters: CSS added after PDF generation cannot affect the file.

Playwright: inject a runtime CSS string

Playwright is a strong choice when your HTML uses browser JavaScript, modern CSS, web fonts, or layout that must match Chromium. Load the HTML, inject the string, wait for assets, choose print or screen media, and create the PDF.

import { chromium } from 'playwright';

const htmlString = `
  <!doctype html>
  <html><head></head><body>
    <h1 class="title">Invoice</h1>
    <p>Generated from an HTML string.</p>
  </body></html>`;

const cssString = `
  @page { size: A4; margin: 18mm; }
  body { font-family: Arial, sans-serif; color: #222; }
  .title { color: #0b5; break-after: avoid; }
  @media print { .screen-only { display: none; } }
`;

const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(htmlString, { waitUntil: 'networkidle' });
await page.addStyleTag({ content: cssString });
await page.emulateMedia({ media: 'print' });
await page.pdf({
  path: 'output.pdf',
  printBackground: true,
  preferCSSPageSize: true
});
await browser.close();

addStyleTag creates a style tag containing the raw CSS. Put it after setContent so the target document exists, but before pdf. Playwright PDF generation uses print CSS by default; explicit emulation makes the choice visible in code. Remove the emulateMedia call only when the default print behavior is what you want.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Assets and base URLs

An HTML string has no natural document URL. Relative images, stylesheets, and fonts therefore need an absolute URL or a page base URL. For local resources, use file URLs carefully; for remote resources, wait until they finish loading. A data URL or embedded image avoids a missing-path problem. If a page depends on JavaScript rendering, wait for a selector that proves the content is ready rather than relying only on a short delay.

#1 Best Overall
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option

Puppeteer: the equivalent browser workflow

Puppeteer exposes the same essential sequence. Its PDF method generates a PDF using the print CSS media type. If your CSS is designed for screens, call page.emulateMediaType('screen') before creating the PDF.

const puppeteer = require('puppeteer');

(async () => {
  const htmlString = `<main><h1>Report</h1><p>Hello PDF</p></main>`;
  const cssString = `
    @page { size: Letter; margin: 0.7in; }
    body { font: 16px/1.5 system-ui, sans-serif; }
    h1 { color: #174ea6; }
  `;

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setContent(htmlString, { waitUntil: 'networkidle0' });
  await page.addStyleTag({ content: cssString });
  // Use this only when the stylesheet is written for screen media:
  // await page.emulateMediaType('screen');
  await page.pdf({
    path: 'output.pdf',
    printBackground: true,
    preferCSSPageSize: true
  });
  await browser.close();
})();

Controlling paper size, margins, and colors

  • @page: Set size and margins in the injected stylesheet. With preferCSSPageSize: true, Chromium gives the CSS page size priority.
  • Renderer options: Set format, landscape orientation, margins, header/footer templates, and page ranges in the PDF options when those are easier to vary per request.
  • Backgrounds: Keep printBackground: true when colored panels or images are part of the design.
  • Exact color output: Print rendering can adjust colors. Add -webkit-print-color-adjust: exact; to the relevant rule when preserving screen colors is more important than printer-friendly output.
  • Pagination: Use break-before, break-after, break-inside: avoid, and @page margin boxes where supported. Test long tables and headings across page boundaries.

WeasyPrint: pass the CSS string as a stylesheet object

WeasyPrint is appropriate for a Python-native, paged-document pipeline. Instead of modifying a browser DOM, create a CSS object from the string and supply it to write_pdf.

from weasyprint import HTML, CSS

html_string = """
<!doctype html>
<html><body>
  <h1 class="title">Report</h1>
  <p>Rendered by WeasyPrint.</p>
</body></html>
"""

css_string = """
@page { size: A4; margin: 18mm; }
body { font-family: sans-serif; color: #222; }
.title { color: #174ea6; }
"""

html = HTML(string=html_string, base_url="https://example.com/")
css = CSS(string=css_string, base_url="https://example.com/")
html.write_pdf("output.pdf", stylesheets=[css])

Set base_url when the HTML or CSS refers to relative images, fonts, or other files. WeasyPrint supports links, bookmarks, and paged-document CSS, but it does not provide the same browser JavaScript and CSS fidelity as Chromium.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Custom fonts with WeasyPrint

For @font-face, create one FontConfiguration and pass it both when constructing the CSS and when writing the PDF.

from weasyprint import HTML, CSS
from weasyprint.text.fonts import FontConfiguration

font_config = FontConfiguration()
css = CSS(
    string="""
    @font-face {
      font-family: Brand;
      src: url('fonts/brand.woff2');
    }
    body { font-family: Brand, sans-serif; }
    """,
    base_url="/srv/templates/",
    font_config=font_config,
)
HTML(string=html_string, base_url="/srv/templates/").write_pdf(
    "output.pdf", stylesheets=[css], font_config=font_config
)

Choosing the renderer

Requirement Prefer Reason
Modern browser CSS, JavaScript, or Chromium-matching layout Playwright or Puppeteer They render a real browser page and allow direct style-tag injection.
Python-only service and explicit stylesheet objects WeasyPrint HTML and CSS strings are first-class inputs to write_pdf.
Screen-specific CSS Either, with deliberate media selection Browser PDFs default to print media; WeasyPrint is designed for paged output.
Heavy client-side application Playwright or Puppeteer They execute browser JavaScript before capture.

Compare more than API syntax: CSS and JavaScript fidelity, font loading, external asset resolution, pagination controls, runtime size, and isolation requirements determine the practical result.

Reliable capture checklist

  1. Validate the HTML and CSS strings before sending them to the renderer.
  2. Inject the CSS before the PDF call; do not rely on a later DOM change.
  3. Choose print or screen media intentionally and keep that choice in code.
  4. Define paper dimensions and margins with @page or renderer options.
  5. Wait for network activity, fonts, images, and application data to finish loading.
  6. Give every relative asset a valid base_url, absolute URL, or embedded data URL.
  7. Use print backgrounds and color-adjust rules when visual fidelity requires them.
  8. Open the resulting PDF in an automated check for page count, expected text, and a representative image.
  9. Run untrusted HTML and CSS in an isolated process or container with a restrictive network and filesystem policy.

Common failures and fixes

The PDF ignores the injected rules

Check that addStyleTag or the WeasyPrint CSS object runs before PDF creation, that the selector actually matches, and that a more-specific rule or an !important declaration is not overriding it. In browser renderers, inspect the page immediately before pdf() if the issue is intermittent.

Screen layout appears different on paper

That is usually media selection, not failed injection. Browser PDF APIs use print media by default. Keep print rules, or call emulateMediaType('screen') / emulateMedia({ media: 'screen' }) when the CSS is intentionally screen-only.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Images, fonts, or stylesheets are missing

Resolve relative URLs with a base URL, use absolute HTTPS URLs, and wait for loading. Protected resources may require authentication headers or cookies. A network-idle event does not guarantee that a late font swap or application request has completed; wait for a meaningful selector or font readiness as well.

Pages break inside cards or rows

Apply break-inside: avoid to the component, move headings with break-after: avoid, and review the available page height after margins are applied. Very large unbreakable elements cannot fit on one page and must be redesigned or allowed to split.

Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers

WeasyPrint cannot load a web font

Verify the font URL and permissions, provide a correct base_url, and use a shared FontConfiguration for both CSS and PDF generation. Browser-only font formats or unsupported CSS may require a fallback or a Chromium renderer.

Rendering is slow or unreliable

Reuse a browser process instead of launching Chromium for every request, but create isolated pages or contexts per job. Limit asset size, set navigation and PDF timeouts, and avoid waiting for global network idle on pages that keep analytics connections open. For WeasyPrint, cache templates and fonts while keeping request-specific data separate.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Security and operational boundaries

HTML and CSS can reference network resources, consume excessive memory, or exploit weaknesses in a privileged rendering environment. Do not pass arbitrary user input to a renderer with unrestricted filesystem or network access. Use process or container isolation, resource limits, allowlists for outbound hosts where possible, and sanitized templates. Treat custom headers, cookies, and authorization data as secrets and never expose them in generated logs or public URLs.

Or skip the browser setup

If your HTML is already available at a URL, ScreenshotNeo can return a screenshot or PDF through one GET request. Its cleaning step accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers.

For an API call, see the ScreenshotNeo documentation:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python and Node.js clients use the same endpoint:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also provides PDF capture, custom CSS and JavaScript, waiting controls, headers and cookies, device and viewport settings, lazy-image loading, signed links, asynchronous jobs, bulk capture, caching, and an MCP server with take_screenshot, get_page_info, and capture_pdf for AI agents. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Start with the free ScreenshotNeo account.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Can I append a CSS string after calling the PDF method?

No. The renderer snapshots the document when PDF generation starts, so inject the style tag or stylesheet object first.

Should I use print or screen media for a PDF?

Use print media for a document designed for paper. Choose screen media explicitly only when your stylesheet depends on screen rules.

Why do relative URLs fail when I pass an HTML string?

A string has no dependable document location. Supply a base URL, convert paths to absolute URLs, or embed the assets.

Is a headless browser required for every HTML-to-PDF job?

No. WeasyPrint handles many static, Python-native documents; browser renderers are preferable when JavaScript or browser CSS fidelity is essential.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.