ScreenshotNeo

BlogHTML to image & PDF

How to Export Plotly Graphs and HTML Content to PDF

Export Plotly figures with Kaleido or print complete HTML pages with Playwright, including setup, print CSS, troubleshooting, and production tips.

By the ScreenshotNeo team30 September 20267 min read

How to Export Plotly Graphs and HTML Content to PDF

Use Plotly’s static image export for one figure; use a real browser for an entire HTML page or dashboard. In Python, install Kaleido and call fig.write_image("figure.pdf"). For complete pages, Playwright’s Chromium renderer and page.pdf() apply print CSS and support paper size, margins, page ranges, backgrounds, and headers or footers.

A PDF is static: it cannot retain Plotly hover, pan, zoom, or legend interactions. Export an interactive companion with fig.write_html("figure.html") when readers need exploration.

1. Choose the right export path

What you have Use Renderer Best for
A Plotly figure object fig.write_image("figure.pdf") Kaleido One chart with predictable figure dimensions
An HTML document, report, or dashboard Playwright page.pdf() Chromium Complete layouts, multiple charts, tables, and CSS
An interactive chart for browser viewing fig.write_html("figure.html") Browser at open time Hover, zoom, pan, and legend controls

Plotly documents PDF as a supported static-image format and HTML as an interactive export. See the Plotly static image export guide and Plotly HTML export guide.

2. Export a Plotly figure to PDF in Python

Install the exporter

python -m pip install --upgrade plotly kaleido

Kaleido 1.0.0 or newer is required by current Plotly releases. Kaleido v1 expects Chrome or Chromium to be installed separately. Plotly documents the plotly_get_chrome command and the plotly.io.get_chrome() helper if your environment does not already provide a browser.

A Plotly figure becomes a static PDF through Kaleido.
A Plotly figure becomes a static PDF through Kaleido.

Complete runnable example

import plotly.express as px

fig = px.bar(
    px.data.gapminder().query("country == 'Canada'"),
    x="year",
    y="pop",
    title="Canada population"
)
fig.update_layout(
    width=1200,
    height=700,
    margin=dict(l=70, r=30, t=80, b=60)
)
fig.write_image("figure.pdf")

The file extension selects PDF. You can also call plotly.io.write_image(fig, "figure.pdf").

Control size and quality

fig.write_image(
    "figure.pdf",
    width=1600,
    height=900,
    scale=2
)

Set width and height on export or in fig.update_layout. A larger scale increases raster resolution for rasterized portions but also increases work and output size. PDF geometry is still governed by the figure dimensions; it is not a paginated document layout engine.

Keep vector output where possible

WebGL traces can put rasterized regions inside an otherwise vector PDF. Plotly Express may switch scatter-style figures to WebGL above 1,000 data points. If fully vector output matters, request SVG rendering or build the trace with go.Scatter:

import plotly.express as px

fig = px.scatter(
    px.data.iris(),
    x="sepal_width",
    y="sepal_length",
    color="species",
    render_mode="svg"
)
fig.write_image("scatter.pdf")

3. Export interactive HTML instead of PDF

import plotly.express as px

fig = px.line(px.data.stocks(), x="date", y="GOOG", title="Google stock")
fig.write_html("figure.html")

Open the resulting file in a browser to retain hover labels, pan, zoom, and legend interactions. Use PDF for a fixed record, print workflow, or attachment; provide both files when users need a printable snapshot and an exploratory chart.

4. Convert a complete HTML page or dashboard with Playwright

Install Playwright and Chromium

npm install playwright
npx playwright install chromium

Complete Node.js script

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });

  await page.goto('http://localhost:3000/report', { waitUntil: 'networkidle' });
  await page.pdf({
    path: 'report.pdf',
    format: 'A4',
    printBackground: true,
    preferCSSPageSize: true,
    margin: { top: '16mm', right: '14mm', bottom: '16mm', left: '14mm' }
  });

  await browser.close();
})();

Playwright’s page.pdf() generates a PDF using print CSS by default. The Playwright page.pdf reference lists paper formats, explicit width and height, margins, page ranges, scale, print backgrounds, tagged PDFs, and CSS page sizing.

Playwright prints the full HTML page with browser layout and print CSS.
Playwright prints the full HTML page with browser layout and print CSS.

Wait for charts, data, and fonts

await page.goto('http://localhost:3000/report', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#dashboard-ready');
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(250);
await page.pdf({ path: 'report.pdf', format: 'A4', printBackground: true });

Have the page set id="dashboard-ready" only after asynchronous data and Plotly rendering finish. A fixed delay alone is less reliable because network and chart times vary.

Screen CSS versus print CSS

await page.emulateMedia({ media: 'screen' });
await page.pdf({ path: 'screen-layout.pdf', printBackground: true });

Use this when the screen layout is the intended source. Otherwise keep the default print media and define print-specific rules:

@page {
  size: A4 portrait;
  margin: 14mm;
}

@media print {
  nav, .chat-widget, .no-print { display: none !important; }
  .chart { break-inside: avoid; }
  h1, h2 { break-after: avoid; }
  body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}

Useful PDF options

  • format: 'A4', or explicit width and height, sets paper dimensions.
  • margin controls each edge.
  • printBackground: true preserves background colors and images.
  • preferCSSPageSize: true lets @page win over the format setting.
  • landscape: true rotates standard paper formats.
  • pageRanges: '1-3' exports selected pages.
  • displayHeaderFooter, headerTemplate, and footerTemplate add repeating running content.
  • scale changes rendered size; check clipping after changing it.

5. Export with cURL, Python, or an API

For a browser-rendered URL, an API can remove local browser installation and produce an image or PDF from one request. ScreenshotNeo accepts a URL and returns a PNG, JPEG, WebP, or PDF. Its API documentation lists all options.

6. Troubleshooting

Symptom Cause Fix
BrowserType.launch cannot find Chromium Playwright’s browser binary is missing. Run npx playwright install chromium in the same environment as the script.
Kaleido reports that Chrome is unavailable Kaleido v1 does not bundle Chrome. Install Chrome or Chromium, or use Plotly’s documented plotly_get_chrome or plotly.io.get_chrome() helper.
PDF is blank or missing charts Export started before client-side data or Plotly finished. Wait for a page readiness selector, document.fonts.ready, and any required network calls.
Colors or backgrounds disappear Print CSS suppresses backgrounds by default. Pass printBackground: true and use print color adjustment CSS.
Screen layout changes in the PDF page.pdf() uses print media. Add print CSS or call page.emulateMedia({ media: 'screen' }).
Charts are clipped or split Fixed heights, page breaks, or margins conflict with paper size. Use responsive chart sizing, break-inside: avoid, explicit margins, and test the selected paper format.
PDF contains rasterized markers The figure uses WebGL traces. Use render_mode='svg' or SVG-based graph objects when vector output is required.
Fonts differ between environments The requested font is unavailable or not loaded before export. Install or self-host the font and await document.fonts.ready.
Interactive controls are absent PDF is a static format. Ship the HTML export alongside the PDF.

7. Reliability, performance, and cost

  • Reliability: pin compatible Plotly, Kaleido, Playwright, and browser versions; make readiness explicit; close browser instances in a finally block; write to a temporary file before replacing the final artifact.
  • Performance: reuse a Playwright browser for batches, create isolated pages, block analytics and video requests when they are not part of the report, and avoid unnecessary export scale. Large WebGL scenes and high-resolution images take longer.
  • Reproducibility: set viewport, timezone, locale, device scale, paper size, margins, and URL data inputs explicitly. Capture at a known commit or report timestamp.
  • Cost: self-hosted Kaleido and Playwright consume your compute and browser maintenance time. A hosted renderer trades that setup for per-capture pricing, so include retries, storage, and PDF transfer in capacity planning.

8. Or skip the browser setup

ScreenshotNeo can render a URL to PDF with one request. It accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms plus newsletter popups and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and each response reports the result in X-Page-Verdict and X-Billed headers.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com/report \
  -d format=pdf \
  -o report.pdf

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={
        "access_key": "YOUR_API_KEY",
        "url": "https://example.com/report",
        "format": "pdf",
    },
    timeout=90,
)
r.raise_for_status()
open("report.pdf", "wb").write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/report',
  format: 'pdf'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
require('fs').writeFileSync('report.pdf', Buffer.from(await res.arrayBuffer()));

Use the API’s PDF options for paper size, margins, landscape mode, and page ranges. You can also set custom CSS or JavaScript, wait for a selector, delay, or network idle, provide headers, cookies, a user agent, authorization, timezone, geolocation, and block selected requests or resource types. Async jobs with signed webhooks, bulk capture of up to 100 URLs per call, caching with a chosen TTL, signed links, and a usage API cover batch pipelines. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

Every feature is included on every plan: 1,000 shots per month free with no card, then Starter is $5 for 3,000 shots; yearly billing gives two months free. Start with the free ScreenshotNeo account.

9. FAQ

Can a Plotly PDF remain interactive?

No. Use write_html for interaction and provide the PDF as a static companion.

Should I use Kaleido or Playwright?

Use Kaleido for a Plotly figure object. Use Playwright when the source is a complete page, dashboard, or report.

Why does my PDF look different from the browser?

Playwright prints with print media by default. Add print CSS, or emulate screen media before calling page.pdf().

How do I export only selected pages?

With Playwright, pass a page range such as pageRanges: '2-4'. For Plotly, compose the desired content before static export.

What is the simplest hosted option?

Use ScreenshotNeo’s PDF endpoint when you want URL-to-PDF rendering without maintaining Chromium, while retaining controls for waiting, CSS, JavaScript, headers, cookies, and page layout.