Guides / HTML to PDF
HTML to PDF

How to Wait for Dynamic Content Before Generating a PDF

Learn how to capture charts, single-page apps, and lazy-loaded data only after they finish rendering.

Overview

Charts, React or Vue apps, and pages that fetch data after loading often come out blank or half-rendered in a PDF. The page has loaded, but its JavaScript hasn't finished drawing yet, so the PDF captures an empty canvas or a loading spinner.

This guide shows how to use the HTML to PDF API options waitForSelector, waitForNetworkIdle, javascript, and delay to capture the page only after it has finished rendering.

Choosing a strategy

Every request already waits for the page's load and DOMContentLoaded events and for the network to settle (no more than 2 open connections). Content that renders after that point needs one of these options:

  • waitForSelector: Waits for an element to appear. Best when you control the page.
  • waitForNetworkIdle: Waits until there are no open network connections. Best for pages you don't control.
  • javascript: Runs your code in the page before capture, e.g. to turn off animations.
  • delay: Waits a fixed number of milliseconds. Use it as a last resort.

The API applies them in this order:

  1. Load the page (plus waitForNetworkIdle, if set).
  2. Wait for waitForSelector.
  3. Run click actions such as clickSelectorChainSetup, if set.
  4. Inject javascript, then css.
  5. Wait for delay.
  6. Wait for images if loadImages is set, then generate the PDF.

waitForSelector

waitForSelector pauses until an element matching a CSS selector exists in the DOM. It is the most precise option: the PDF is generated as soon as the element appears, with no guessing. In this example, the API waits for a #chart-ready element before capturing the page.

  • url: The URL of the page you want to convert.
  • waitForSelector: The CSS selector to wait for.
CURL
NODE.JS
1curl https://api.pdfgate.com/generate/pdf \2  -X POST \3  -H "Content-Type: application/json" \4  -H "Authorization: Bearer YOUR_API_KEY" \5  --data '{6    "url": "https://example.com/reports/sales",7    "waitForSelector": "#chart-ready"8  }' \9  -o output.pdf

The selector must appear within 30 seconds. If it doesn't, the request fails with a 422 error instead of returning an incomplete PDF.

Signal "ready" from your page

Waiting for the chart's own canvasor a table's container isn't enough, because those elements usually exist before they are filled in. The reliable pattern is to have the page add a marker element or attribute once rendering is done, then point waitForSelectorat it. The marker doesn't need to be visible; it only has to exist in the DOM.

Chart.js example

Add the marker in the chart's animation.onComplete callback, then send "waitForSelector": "#chart-ready". This works the same whether you send the page with url or as raw html.

1<canvas id="sales-chart"></canvas>23<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>4<script>5  new Chart(document.getElementById("sales-chart"), {6    type: "bar",7    data: {8      labels: ["Q1", "Q2", "Q3", "Q4"],9      datasets: [{ label: "Sales", data: [120, 190, 150, 210] }],10    },11    options: {12      animation: {13        // Called when the chart has finished drawing14        onComplete: function () {15          if (!document.getElementById("chart-ready")) {16            const marker = document.createElement("div");17            marker.id = "chart-ready";18            document.body.appendChild(marker);19          }20        },21      },22    },23  });24</script>

React example

Render a data-pdf-ready attribute only after the data has loaded, then send "waitForSelector": "[data-pdf-ready]". In Vue, use the same attribute on an element guarded by v-if.

1function SalesReport() {2  const [data, setData] = useState(null);34  useEffect(() => {5    fetch("/api/sales")6      .then((res) => res.json())7      .then(setData);8  }, []);910  if (!data) return <p>Loading…</p>;1112  // The attribute only exists once the real content is rendered13  return (14    <div data-pdf-ready="true">15      <SalesTable rows={data.rows} />16    </div>17  );18}

If a page has several charts or data sources, add the marker only when all of them are done.

waitForNetworkIdle

When you can't change the page, waitForNetworkIdle is the next best option. It makes the page load wait until there are no open network connections, so data fetched after the initial load has time to arrive.

  • waitForNetworkIdle: Set to true to wait for zero network connections.
CURL
NODE.JS
1curl https://api.pdfgate.com/generate/pdf \2  -X POST \3  -H "Content-Type: application/json" \4  -H "Authorization: Bearer YOUR_API_KEY" \5  --data '{6    "url": "https://example.com/dashboard",7    "waitForNetworkIdle": true8  }' \9  -o output.pdf

Keep in mind:

  • Pages that poll an API, stream analytics, or keep a WebSocket or long-polling connection open may never go idle. The request then fails once timeout is reached, so set a lower timeout to fail fast.
  • A quiet network doesn't mean rendering is finished. Chart animations and client-side rendering can continue after the last request completes.

Inject JavaScript

javascript injects a script into the page just before capture. Use it to put the page into a print-ready state, for example by finishing chart animations immediately. This example turns off animations on every Chart.js chart on the page so they render in their final state.

  • javascript: The JavaScript code to run in the page.
CURL
NODE.JS
1curl https://api.pdfgate.com/generate/pdf \2  -X POST \3  -H "Content-Type: application/json" \4  -H "Authorization: Bearer YOUR_API_KEY" \5  --data '{6    "url": "https://example.com/reports/sales",7    "javascript": "Object.values(Chart.instances).forEach(function (chart) { chart.options.animation = false; chart.update(\"none\"); });"8  }' \9  -o output.pdf

The script runs after waitForSelector, so an element your script creates can't be used as the selector to wait for. The API also doesn't wait for asynchronous work your script starts. If the script triggers new content, such as scrolling to load more items, pair it with delay:

1{2  "url": "https://example.com/products",3  "javascript": "window.scrollTo(0, document.body.scrollHeight);",4  "delay": 20005}

For lazy-loaded images specifically, set loadImages to true instead. It scrolls through the page and waits for images to finish loading.

delay

delay waits a fixed number of milliseconds before generating the PDF. The maximum is 20000 (20 seconds).

  • delay: The time to wait, in milliseconds.
CURL
NODE.JS
1curl https://api.pdfgate.com/generate/pdf \2  -X POST \3  -H "Content-Type: application/json" \4  -H "Authorization: Bearer YOUR_API_KEY" \5  --data '{6    "url": "https://example.com/dashboard",7    "delay": 30008  }' \9  -o output.pdf

A fixed delay is a guess. If your data source is slow, the PDF is still incomplete; if it's fast, every request waits longer than it needs to. Prefer waitForSelector when you can, and use delayonly for pages you can't change and that never go network-idle.

Additional notes

  • timeout limits how long the page may take to load, in milliseconds. The default and maximum is 900000 (15 minutes). It covers the waitForNetworkIdle wait but not waitForSelector, which always has its own 30-second limit.
  • You can combine options. For example, waitForNetworkIdle to let data arrive, then javascript to disable animations. Each one adds to the total generation time.
  • To control how the rendered content is laid out on the page, see Page size and margins.

For a full list of supported options, see the HTML to PDF API Reference.