ScreenshotNeo

BlogHTML to image & PDF

How to Include Form Inputs in Node.js Puppeteer PDFs

Set form values before page.pdf(), preserve them with print CSS, and avoid common Puppeteer PDF input and fillability problems.

By the ScreenshotNeo team30 September 202610 min read

How to Include Form Inputs in Node.js Puppeteer PDFs

To include form inputs in a Node.js Puppeteer PDF, set each control to the value you want displayed before calling page.pdf(). Puppeteer prints the page using the print CSS media type by default, so your print stylesheet must keep those controls visible and readable. The resulting PDF is a rendered snapshot; it is not automatically an editable PDF form.

This guide covers text fields, textareas, checkboxes, radio buttons, selects, custom controls, print CSS, PDF options, validation, troubleshooting, and production considerations.

1. What Puppeteer puts in the PDF

page.pdf() prints the current rendered state of the page. It does not inspect your form model and reconstruct values later. The browser must already contain the desired values when PDF generation starts.

For example, if an input initially contains an empty value and your script never changes it, the PDF will show an empty control. If JavaScript fills the input after an API request, wait for that request and the resulting DOM update before printing.

Puppeteer documents Page.pdf() as generating a PDF with the print CSS media type. See the Page.pdf() API reference and the PDF generation guide.

2. Complete Node.js example

The following script opens a form, fills several native controls, waits for the page to settle, and writes an A4 PDF.

Set every control value before Puppeteer prints the rendered page.
Set every control value before Puppeteer prints the rendered page.
const puppeteer = require('puppeteer');

async function createPdf() {
  const browser = await puppeteer.launch();

  try {
    const page = await browser.newPage();

    await page.goto('https://example.com/form', {
      waitUntil: 'networkidle2'
    });

    await page.locator('input[name="name"]').fill('Ada Lovelace');
    await page.locator('input[name="email"]').fill('ada@example.com');
    await page.locator('textarea[name="notes"]').fill('Reviewed and approved.');
    await page.select('select[name="category"]', 'approved');

    await page.locator('input[name="terms"]').click();
    await page.locator('input[name="priority"][value="high"]').click();

    // Allow application code to react to the new values.
    await page.waitForFunction(() => {
      const status = document.querySelector('[data-form-status]');
      return status && status.textContent.includes('Ready');
    });

    await page.pdf({
      path: 'form.pdf',
      format: 'A4',
      printBackground: true,
      margin: {
        top: '16mm',
        right: '16mm',
        bottom: '16mm',
        left: '16mm'
      },
      waitForFonts: true
    });
  } finally {
    await browser.close();
  }
}

createPdf().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

The exact selectors and values depend on the page. Puppeteer’s locator APIs support filling controls, while page.select() selects an option in a native <select>. The locator fill documentation and page.select documentation describe these interactions.

3. Set each kind of form control correctly

Text inputs

Use a locator that identifies the field and call fill(). This replaces the current value and dispatches the events expected by normal page code.

await page.locator('input[name="name"]').fill('Ada Lovelace');
await page.locator('input[type="date"]').fill('1843-07-01');
await page.locator('input[type="number"]').fill('42');

For inputs with validation, verify that the value is accepted before printing:

await page.waitForFunction(() => {
  const input = document.querySelector('input[name="email"]');
  return input && input.value === 'ada@example.com' && input.checkValidity();
});

Textareas

await page.locator('textarea[name="notes"]').fill('The long review note appears in the PDF.');

Long text can change pagination. Use print CSS to control wrapping and page breaks, then inspect the generated PDF with representative long and short values.

Native selects

await page.select('select[name="category"]', 'approved');

The argument is the option’s value, not necessarily the visible label. Confirm the selection:

const selected = await page.$eval(
  'select[name="category"]',
  (select) => select.value
);
if (selected !== 'approved') {
  throw new Error(`Unexpected category: ${selected}`);
}

Checkboxes and radio buttons

Click the control only when its current state is not already correct. This avoids accidentally toggling a checkbox off.

await page.$eval('input[name="terms"]', (input) => {
  if (!input.checked) input.click();
});

await page.locator('input[name="delivery"][value="express"]').click();

Wait for dependent UI after a click if the application reveals additional fields, recalculates totals, or loads data.

Custom controls

Many design systems render a button, listbox, or combobox instead of a native select. Interact with the visible control using its role or stable test selector, then wait for the selected state your application exposes.

await page.getByRole('combobox', { name: 'Category' }).click();
await page.getByRole('option', { name: 'Approved' }).click();
await page.waitForFunction(() => {
  const control = document.querySelector('[aria-label="Category"]');
  return control?.getAttribute('aria-valuetext') === 'Approved';
});

If a custom widget stores its value only in application state, make sure the visible label or rendered value is updated before calling page.pdf().

4. Make form values visible with print CSS

Print media rules can hide controls, replace them with a compact summary, remove backgrounds, or change layout. Add an explicit print stylesheet for the PDF view.

@media print {
  .screen-only,
  .cookie-banner,
  .app-toolbar {
    display: none !important;
  }

  form,
  .form-section {
    display: block !important;
    visibility: visible !important;
  }

  input,
  textarea,
  select,
  [role="combobox"] {
    color: #111 !important;
    background: #fff !important;
    border: 1px solid #555 !important;
    opacity: 1 !important;
  }

  textarea {
    white-space: pre-wrap;
    overflow: visible;
  }

  .form-section {
    break-inside: avoid;
  }

  .page-break-before {
    break-before: page;
  }

  * {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
}

Use page.emulateMediaType('screen') before PDF generation when the screen design must be printed instead of the print stylesheet:

await page.emulateMediaType('screen');
await page.pdf({ path: 'screen-styled-form.pdf', printBackground: true });

Screen styling can produce poor pagination, so a dedicated print stylesheet is usually easier to maintain. Puppeteer’s documentation explains the print media behavior and the screen-media override.

5. Wait for application state, fonts, and images

waitUntil: 'networkidle2' waits for a quiet navigation, but it cannot know whether your form’s client-side state is ready. Add a page-specific readiness condition.

await page.goto(url, { waitUntil: 'networkidle2' });
await page.locator('input[name="name"]').fill('Ada Lovelace');
await page.locator('select[name="category"]').select('approved');

await page.waitForSelector('[data-pdf-ready="true"]');
await page.evaluate(() => document.fonts.ready);

await page.pdf({
  path: 'form.pdf',
  format: 'A4',
  printBackground: true,
  waitForFonts: true
});

Use a deterministic marker such as data-pdf-ready after all asynchronous calculations finish. For remote images, wait for the relevant image elements to complete or use a page-level readiness signal. Avoid arbitrary delays unless the page has no better observable condition.

6. PDF sizing and output options

Choose options based on the document rather than relying on defaults. Puppeteer documents these commonly used settings:

Option Use Considerations
format Standard paper such as A4 or letter The current documented default is letter.
width, height Custom page dimensions Useful for receipts or fixed layouts.
margin Reserve printable space Use CSS units such as mm, in, or px.
landscape Rotate the page Helpful for wide tables and form sections.
scale Scale rendered output Check readability after scaling.
printBackground Include backgrounds and colors The documented default is false; set it explicitly when needed.
preferCSSPageSize Honor CSS @page sizing Useful when page dimensions are defined in CSS.
waitForFonts Wait for fonts before printing The documented default is true.
tagged Request tagged PDF output The current reference documents it as experimental with a default of true.
await page.pdf({
  path: 'wide-form.pdf',
  format: 'A4',
  landscape: true,
  printBackground: true,
  preferCSSPageSize: true,
  scale: 0.95,
  margin: { top: '12mm', right: '12mm', bottom: '12mm', left: '12mm' }
});

Print output can modify colors. The -webkit-print-color-adjust: exact rule requests closer color preservation, but verify the actual PDF produced by the Puppeteer and Chromium versions used in deployment.

7. Rendered values are not editable PDF fields

A visible input value in a Puppeteer PDF is normally painted content. A reader can see “Ada Lovelace,” but the PDF does not automatically contain a text field that can be edited in Acrobat or another PDF viewer.

If the requirement is a fillable PDF, treat it as a separate document-production task. Generate the rendered PDF with Puppeteer, then use a PDF form-authoring or post-processing workflow that creates interactive fields. Define field names, types, tab order, validation, and appearance separately, and test the result in the PDF readers your users use. Puppeteer’s page-printing API alone does not establish that behavior.

8. Troubleshooting common failures

Input values are missing

Cause: the script prints before setting the values, or a later React/Vue update replaces them.

Fix: fill the controls first, wait for the application’s ready marker, then call page.pdf(). Read the values with page.$eval() immediately before printing to confirm the rendered state.

The select shows the wrong option

Cause: the script passed the visible label instead of the option’s value, or a custom select is not a native <select>.

Fix: inspect the option values and use page.select() for native controls. For custom widgets, click the control and option, then wait for its ARIA state or visible label.

Controls disappear in the PDF

Cause: @media print rules set the form or its parent to display:none, visibility:hidden, or zero opacity.

Fix: override those rules in a print stylesheet. If the screen layout is intentionally required, call page.emulateMediaType('screen').

The PDF contains stale calculated values

Cause: form events trigger asynchronous calculations that have not completed.

Fix: wait for a specific result such as a total, status element, or data-pdf-ready marker. A fixed timeout is less reliable than an observable condition.

Text is clipped or split badly

Cause: fixed heights, overflow rules, large controls, or page-break behavior conflict with print dimensions.

Fix: remove fixed heights in print CSS, allow textareas to expand, set suitable margins, and use break-inside: avoid for sections that should stay together. Test long values.

Colors or backgrounds are absent

Cause: printBackground defaults to false.

Fix: pass printBackground: true and add -webkit-print-color-adjust: exact where exact colors matter.

Fonts change the layout

Cause: the PDF is generated before web fonts finish loading, or the deployed environment cannot reach the font files.

Fix: use waitForFonts: true, await document.fonts.ready, and ensure font requests succeed in the runtime environment.

The PDF is visible but not fillable

Cause: Puppeteer printed rendered page content rather than creating interactive AcroForm fields.

Fix: add a dedicated PDF form-authoring or post-processing step and test fields in the target PDF reader.

9. Reliability, performance, and cost considerations

  • Reuse browsers carefully: launching Chromium for every document adds startup cost. A long-running worker can reuse a browser while creating a fresh page per job. Close pages after each job and restart the browser under a policy appropriate for your workload.
  • Control navigation: use explicit timeouts and readiness markers. A page that never reaches network idle can delay a job indefinitely if no timeout is configured.
  • Keep PDFs deterministic: pin the Puppeteer and Chromium versions used in production, load the same fonts, and avoid time-dependent content when visual consistency matters.
  • Measure document size: large images, backgrounds, and long forms increase memory use and output size. Resize assets and omit unnecessary screen-only elements in print CSS.
  • Validate representative data: test empty fields, maximum-length text, selected and unselected controls, validation errors, multiple pages, slow API responses, and missing optional data.
  • Protect private forms: do not expose credentials or sensitive form data in logs, URLs, screenshots, or temporary files. Delete generated files according to your retention policy.

10. Or skip the browser setup

If you need a clean capture or PDF without maintaining Chromium automation, ScreenshotNeo provides a website screenshot API and MCP server. It accepts one GET request and returns a PNG, JPEG, WebP, or PDF. Its cleanup step accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled.

A clean capture pipeline removes common overlays before producing the output.
A clean capture pipeline removes common overlays before producing the output.

For a page whose form has already been populated by its own URL or server-rendered state, call the API directly:

cURL

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

Python

import requests

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

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/form'
});

const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo request failed: ${res.status}`);
const file = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then((fs) => fs.writeFile('form.webp', file));

See the ScreenshotNeo API documentation for request options. Relevant options include PDF paper size, margins, landscape mode, page ranges, custom CSS and JavaScript, waiting for a selector, a delay or network idle, custom headers and cookies, authentication, timezone and geolocation, full-page capture, element capture, hiding selectors, blocking resource types, caching, signed links, asynchronous jobs, webhooks, bulk capture, and usage reporting.

ScreenshotNeo reports X-Page-Verdict and X-Billed headers. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; only clean shots are billed. An MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

11. Practical checklist

  1. Navigate to the form and wait for the required page state.
  2. Fill text inputs and textareas.
  3. Select native options by their values.
  4. Set checkbox and radio states without accidental toggles.
  5. Interact with custom controls through stable selectors or roles.
  6. Wait for calculations, dependent fields, fonts, and images.
  7. Confirm values in the DOM immediately before printing.
  8. Apply print CSS that keeps controls visible and readable.
  9. Choose paper, margins, background, scale, and page-break options.
  10. Inspect the PDF and confirm whether the requirement is rendered content or editable fields.

12. FAQ

Does Puppeteer preserve the value property or only the original HTML?

It prints the current rendered page. Set the live control value before printing and confirm that the visible state has updated.

Can I include browser validation messages?

Yes, if they are visible in the rendered page and your print CSS does not hide them. Trigger validation deliberately and wait for the message before printing.

Should I use a delay instead of network idle?

Use a page-specific readiness condition whenever possible. Delays can help with third-party behavior but are less predictable than waiting for a known DOM state.

Can a Puppeteer PDF contain editable checkboxes?

Not automatically. Puppeteer prints the checkbox appearance. Interactive PDF fields require separate PDF form creation or post-processing.

Why does the PDF look different from the browser tab?

PDF generation uses print media by default, and print layout can change colors, visibility, sizing, and pagination. Add print CSS or emulate screen media explicitly.