ScreenshotNeo

BlogHTML to image & PDF

How to Export HTML Select Options with html2pdf, html2canvas, and jsPDF

Export every HTML select option to a readable PDF with html2pdf.js, html2canvas, and jsPDF, including pagination, styling, and troubleshooting.

By the ScreenshotNeo team30 September 20269 min read

How to Export HTML Select Options with html2pdf, html2canvas, and jsPDF

Direct answer: do not rely on capturing a native <select> control to print every item in its popup. A browser normally renders the control’s current value, while the opened option menu is managed by the browser and is not a stable document surface for html2canvas. Read select.options, create an export-only list or table containing each option, and pass that ordinary HTML element to html2pdf.js. html2pdf.js uses html2canvas for the canvas rendering step and jsPDF for PDF creation.

This approach gives you explicit content, predictable pagination, and a place to show values or the selected state. It produces a static PDF representation, not an interactive native dropdown or PDF form field.

What “export select options” can mean

Clarify the desired output before writing code:

Goal Recommended output Reason
Show the current choice Capture the form control or render its selected label Compact and usually sufficient for a summary
Show every available choice Build an export-only list or table from select.options Explicitly includes options that are not selected
Preserve a selectable field in the PDF Create PDF form fields separately with a PDF-generation library html2pdf’s canvas image is not a native interactive dropdown

The rest of this guide implements the second case: a readable PDF containing all choices, with an optional marker for the current selection.

How the html2pdf.js pipeline works

  1. Your code creates or selects an HTML element.
  2. html2pdf clones and renders that element through html2canvas.
  3. The resulting canvas is converted to an image.
  4. jsPDF places the image into one or more PDF pages.

Because html2canvas reconstructs the page from the DOM and CSS instead of taking a native operating-system screenshot, CSS support and form-control behavior can differ between browsers. Read the html2canvas documentation before depending on unusual CSS, cross-origin images, or browser-specific controls.

Complete runnable example

The example below keeps the real form control for users and creates a separate export container when the button is pressed. It includes option text, values, and a selected marker. Remove the value column or marker if your document should contain labels only.

Enumerating options into ordinary HTML makes every choice available to the PDF renderer.
Enumerating options into ordinary HTML makes every choice available to the PDF renderer.
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Export select options</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js" defer></script>
  <style>
    body { font: 16px system-ui, sans-serif; margin: 2rem; }
    .export-document { background: #fff; color: #111; padding: 24px; width: 720px; }
    .export-document h1 { margin-top: 0; font-size: 24px; }
    .options-table { border-collapse: collapse; width: 100%; }
    .options-table th, .options-table td { border: 1px solid #bbb; padding: 8px; text-align: left; }
    .options-table thead { display: table-header-group; }
    .options-table tr { break-inside: avoid; page-break-inside: avoid; }
    .selected { font-weight: 700; }
    .export-only { position: absolute; left: -100000px; top: 0; }
  </style>
</head>
<body>
  <label for="plan">Plan</label>
  <select id="plan">
    <option value="free">Free</option>
    <option value="starter" selected>Starter</option>
    <option value="growth">Growth</option>
    <option value="pro">Pro</option>
  </select>
  <button id="download" type="button">Download PDF</button>
  <div id="export-root" class="export-only" aria-hidden="true"></div>

  <script>
    const select = document.querySelector('#plan');
    const exportRoot = document.querySelector('#export-root');

    function buildExportDocument(selectElement) {
      const wrapper = document.createElement('section');
      wrapper.className = 'export-document';
      const heading = document.createElement('h1');
      heading.textContent = 'Available plans';
      wrapper.appendChild(heading);

      const table = document.createElement('table');
      table.className = 'options-table';
      table.innerHTML = '<thead><tr><th>Option</th><th>Value</th><th>Selected</th></tr></thead>';
      const body = document.createElement('tbody');

      for (const option of selectElement.options) {
        const row = document.createElement('tr');
        if (option.selected) row.className = 'selected';
        const labelCell = document.createElement('td');
        labelCell.textContent = option.text;
        const valueCell = document.createElement('td');
        valueCell.textContent = option.value;
        const selectedCell = document.createElement('td');
        selectedCell.textContent = option.selected ? 'Yes' : '';
        row.append(labelCell, valueCell, selectedCell);
        body.appendChild(row);
      }
      table.appendChild(body);
      wrapper.appendChild(table);
      return wrapper;
    }

    document.querySelector('#download').addEventListener('click', async () => {
      exportRoot.replaceChildren(buildExportDocument(select));
      const element = exportRoot.firstElementChild;
      const options = {
        margin: [12, 12, 12, 12],
        filename: 'select-options.pdf',
        image: { type: 'jpeg', quality: 0.95 },
        pagebreak: { mode: ['css', 'legacy'] },
        html2canvas: { scale: 2, backgroundColor: '#ffffff' },
        jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
      };
      await html2pdf().set(options).from(element).save();
    });
  </script>
</body>
</html>

The export element is positioned far off-screen rather than set to display:none. A display-none subtree has no layout to render. If you prefer, append the element visibly in a print-only panel and remove it after generation.

Build the export list safely

Use the option label and value deliberately

option.text is the user-facing label. option.value may be an internal identifier that readers do not need. Decide whether the PDF is documentation for people, an audit record, or a data export. For a human-facing document, labels alone are often clearer. For an audit, include both and preserve the selected marker.

Handle disabled, grouped, and empty options

The options collection includes disabled options and options inside <optgroup>. Preserve that context when it matters:

for (const option of select.options) {
  const group = option.parentElement instanceof HTMLOptGroupElement
    ? option.parentElement.label
    : '';
  const status = option.disabled ? 'Disabled' : 'Available';
  // Render option.text, option.value, group, status, and option.selected.
}

An empty value is valid. Do not filter it with a truthiness check; test explicitly for option.value === ''. If the select allows multiple choices, use option.selected for each row rather than select.value, which represents only one value.

Prevent injection and broken markup

Use textContent when inserting labels and values. Avoid concatenating untrusted option text into innerHTML. If you need rich formatting, construct each element with DOM APIs and set only the attributes you control.

html2pdf.js options that matter

Option Use Practical guidance
margin Page margins in the jsPDF unit Leave room for readable borders and headers
filename Download name Use a stable, filesystem-safe name
image.type, quality Intermediate image format JPEG is smaller; PNG is sharper for line art but can be larger
html2canvas.scale Raster resolution Increase for small text, but expect more memory and time
html2canvas.backgroundColor Canvas background Set white when transparent or inherited backgrounds would be ambiguous
jsPDF.format Paper size Use a4, letter, or another supported format
jsPDF.orientation Portrait or landscape Landscape helps wide value columns
pagebreak.mode Break calculation Use CSS breaks and legacy handling for long tables

The complete option set and worker chain are documented in the html2pdf.js options reference. Keep table rows together with break-inside: avoid, but verify the result because a row taller than a page cannot remain intact.

Clone-time changes and ignored elements

html2canvas supports an onclone callback that receives the cloned document. This is useful when the live page has controls that should be hidden only in the PDF, or when you need export-only styling. Its configuration also includes ignoreElements and the data-html2canvas-ignore attribute. See the configuration reference.

const options = {
  html2canvas: {
    onclone: (clonedDocument) => {
      clonedDocument.querySelectorAll('[data-screen-only]').forEach((node) => {
        node.remove();
      });
      clonedDocument.querySelector('.export-document')?.classList.add('pdf-mode');
    },
    ignoreElements: (element) => element.matches('.live-help-widget')
  }
};

Do not mutate the user’s live form just to make the PDF. Build the export representation or modify the clone instead.

Long lists, page breaks, and layout

  1. Give the export document a predictable width and background.
  2. Use a real table header so repeated headers can be applied by the renderer where supported.
  3. Apply break-inside: avoid to rows and headings.
  4. Test lists that exceed one page; clipping often comes from fixed heights or overflow rules inherited from the application.
  5. Try portrait for two or three narrow columns and landscape for many columns.

For very large lists, consider generating smaller sections and merging PDFs with a server-side PDF tool. A high canvas scale multiplies memory usage, so lower it when the document is mostly text and the output is already readable.

Browser and rendering caveats

html2canvas is a DOM renderer, not a pixel-perfect browser screenshot engine. Unsupported CSS, web fonts that have not finished loading, cross-origin images without appropriate permissions, and native form-control differences can change the result. Wait for fonts and images before capture when they are part of the export. Inspect the generated PDF in every browser you support.

A dated issue report describes stale changed form data with html2canvas 1.4.1 in Firefox 124 and Safari on iOS 17. It is a version- and browser-specific case report, not proof that every release behaves the same way. If a selected value appears stale, reproduce it with your exact library and browser versions; rebuilding the export table from select.options immediately before calling html2pdf avoids relying on a cloned native control’s state. See issue #3166.

Common errors and fixes

Symptom Likely cause Fix
Only the selected option appears The native select was captured Enumerate select.options into ordinary rows
PDF is blank Element is display:none, has zero dimensions, or capture ran before layout Render it off-screen with dimensions and wait until it is attached
Rows are cut between pages Fixed heights or missing break rules Remove fixed heights and add break-inside: avoid
Text is blurry Low canvas scale or oversized rasterization Increase scale gradually and check memory
Images are missing Cross-origin restrictions Use same-origin assets or configure CORS; omit decorative images if they are not essential
Fonts differ Web font has not loaded Await document.fonts.ready before capture
Download never completes Capture failed or the page ran out of memory Reduce scale, simplify the export DOM, and catch the returned promise
await document.fonts.ready;
const images = [...document.images];
await Promise.all(images.map((img) => img.complete
  ? Promise.resolve()
  : new Promise((resolve) => {
      img.addEventListener('load', resolve, { once: true });
      img.addEventListener('error', resolve, { once: true });
    })));
try {
  await html2pdf().set(options).from(element).save();
} catch (error) {
  console.error('PDF generation failed', error);
}

Or skip the browser setup

If your goal is a clean capture of a web page rather than a client-side PDF assembled from a select, ScreenshotNeo provides a single website screenshot API call. Its capture options include PDF output, custom CSS and JavaScript, waiting for selectors or network idle, full-page capture, and hiding selectors. Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. An MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf.

A managed capture can remove common overlays before rendering the final page.
A managed capture can remove common overlays before rendering the final page.

See the ScreenshotNeo API documentation for all options. The same request works from cURL, Python, or Node.js:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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}`);

There is a free tier of 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.

Performance, reliability, and cost checklist

  • Build only the content needed for the PDF; a smaller DOM captures faster.
  • Use a moderate canvas scale and measure output readability on the target paper size.
  • Wait for fonts and required images, but do not wait indefinitely for optional analytics or chat resources.
  • Generate from the current options collection at click time so dynamic selections are correct.
  • For repeatable server workflows, log the library versions, browser versions, input option count, and output page count.
  • Client-side generation uses the user’s CPU and memory; large tables can freeze a low-powered device.
  • ScreenshotNeo charges only for clean shots; bot checks, blank pages, timeouts, failed loads, and cache hits are free.

FAQ

Can html2pdf export the opened dropdown menu?

Do not depend on it. The popup is browser UI rather than stable page content. Render the options as HTML first.

Will the PDF retain the select as an interactive field?

No. html2pdf produces a static PDF image/content flow. Interactive AcroForm fields require a separate PDF-form implementation.

Should I include option values?

Include them for technical or audit documents; omit them for a reader-facing list when labels are sufficient.

How do I export a multiple-select control?

Iterate through every option and mark each option whose selected property is true.

Can I capture a page hosted on another domain with ScreenshotNeo?

Yes, pass its URL to the API and configure the capture options described in the documentation. The service handles the browser session and returns the requested image or PDF.