ScreenshotNeo

BlogHow-to

How to Capture the Selected Dropdown Value with dom-to-image

Show the live value of an HTML select in dom-to-image exports, including labels, multi-selects, cloning, troubleshooting, and a hosted API option.

By the ScreenshotNeo team1 October 20268 min read

How to Capture the Selected Dropdown Value with dom-to-image

Read the live value from the <select>, then render that value as ordinary capture content before calling dom-to-image. A native dropdown’s current selection is browser state. The HTML selected attribute represents the default option and may not change when a user chooses another option. A capture-only label avoids relying on how a cloned native control is painted.

Quick solution

For a single-select control, use select.value for the submitted value. To display the human-readable label, read select.options[select.selectedIndex].text. Put the result in an element inside the node you pass to dom-to-image.

Read the live selection and render it as ordinary capture content before exporting.
Read the live selection and render it as ordinary capture content before exporting.
<select id="choice">
  <option value="basic">Basic plan</option>
  <option value="pro" selected>Pro plan</option>
  <option value="team">Team plan</option>
</select>

<section id="capture-area">
  <h1>Plan summary</h1>
  <p>Selected plan: <strong id="capture-choice"></strong></p>
</section>

<button id="export" type="button">Export PNG</button>

<script src="https://unpkg.com/dom-to-image@2.6.0/src/dom-to-image.js"></script>
<script>
  const select = document.querySelector('#choice');
  const captureLabel = document.querySelector('#capture-choice');
  const captureArea = document.querySelector('#capture-area');

  function syncCaptureLabel() {
    const option = select.options[select.selectedIndex];
    captureLabel.textContent = option ? option.text : '';
  }

  select.addEventListener('change', syncCaptureLabel);
  syncCaptureLabel();

  document.querySelector('#export').addEventListener('click', async () => {
    syncCaptureLabel();
    try {
      const dataUrl = await domtoimage.toPng(captureArea, {
        bgcolor: '#ffffff'
      });
      const image = new Image();
      image.src = dataUrl;
      image.alt = 'Exported plan summary';
      document.body.appendChild(image);
    } catch (error) {
      console.error('dom-to-image export failed:', error);
    }
  });
</script>

The synchronization happens immediately before capture as well as on change. That protects exports triggered by code that changes the selection without dispatching a user event.

Value versus label

What the image should show Expression Example result
Submitted value select.value pro
Visible option label select.options[select.selectedIndex]?.text ?? '' Pro plan
All selected values in a multiple-select [...select.selectedOptions].map(option => option.value) ['pro', 'team']
All selected labels [...select.selectedOptions].map(option => option.text) ['Pro plan', 'Team plan']

Multiple-select example

const select = document.querySelector('#features');
const output = document.querySelector('#capture-features');

function syncMultipleSelect() {
  const labels = [...select.selectedOptions].map(option => option.text);
  output.textContent = labels.length ? labels.join(', ') : 'None selected';
}

select.addEventListener('change', syncMultipleSelect);
syncMultipleSelect();

await domtoimage.toPng(document.querySelector('#capture-area'));

Do not assume that selectedIndex exists for a multiple-select in the way you need; selectedOptions is the direct representation of all current choices. The HTMLSelectElement.value documentation and selectedOptions documentation describe these live properties.

Why dom-to-image can show the default option

dom-to-image clones the supplied node, copies computed styles, serializes the clone into SVG using <foreignObject>, and rasterizes the result. See the dom-to-image README for the documented pipeline and options. The HTML selected attribute establishes an option’s initial state; the live selected state is held by the option’s selected property. Changing the selection does not necessarily rewrite the original markup. Consequently, serialized clone markup can retain the initial selection unless you express the current choice as ordinary text or synchronize state in a clone. This is an inference from the documented pipeline and browser state model, so verify the exact browser and package version used by your application.

Capture-only markup patterns

Use a visible label next to the real control

Keep the native control for interaction and add a separate text node for export. This is the most predictable pattern because the image renderer captures normal HTML text rather than a browser-owned widget.

<label for="country">Country</label>
<select id="country">...</select>
<span id="country-for-image" aria-hidden="true"></span>

Clone the area and synchronize selected state

If the closed native select appearance is part of the design, clone the capture area, find the corresponding option in the clone, and set its selected property and attribute before rendering. This can help, but native controls are browser-rendered and may still differ between engines. A styled capture-only label is generally more stable.

const source = document.querySelector('#capture-area');
const clone = source.cloneNode(true);
const sourceSelect = source.querySelector('#choice');
const cloneSelect = clone.querySelector('#choice');

cloneSelect.value = sourceSelect.value;
for (const option of cloneSelect.options) {
  option.toggleAttribute('selected', option.value === sourceSelect.value);
}

clone.style.position = 'fixed';
clone.style.left = '-100000px';
document.body.appendChild(clone);
try {
  const dataUrl = await domtoimage.toPng(clone);
  // use dataUrl
} finally {
  clone.remove();
}

dom-to-image output methods and options

The library accepts a DOM node and returns promises. Choose the method that matches your delivery format:

Method Use
toPng(node, options) PNG data URL with lossless output.
toJpeg(node, options) JPEG data URL; set quality from 0 to 1.
toBlob(node, options) Blob for downloads or uploads without keeping a large data URL.
toSvg(node, options) Serialized SVG output.
toPixelData(node, options) Raw pixel data for custom processing.

Documented options include:

  • filter(node): return false to omit a node and its subtree.
  • bgcolor: force a background color, useful when transparent output is undesirable.
  • width and height: override the rendered dimensions.
  • style: apply temporary style properties to the clone.
  • quality: JPEG quality.
  • cacheBust: append a cache-busting query string when loading resources.
  • imagePlaceholder: substitute a placeholder when an image cannot be loaded.
const blob = await domtoimage.toBlob(document.querySelector('#capture-area'), {
  bgcolor: '#fff',
  cacheBust: true,
  style: {
    transform: 'scale(1)',
    transformOrigin: 'top left'
  },
  filter: node => !node.matches('.exclude-from-export')
});

const link = document.createElement('a');
link.download = 'plan-summary.png';
link.href = URL.createObjectURL(blob);
link.click();
URL.revokeObjectURL(link.href);

Complete cURL, Python, and Node.js examples

For a browser-side dom-to-image export, the JavaScript example above is the runnable implementation. If you need a server-side screenshot instead, these request forms show the equivalent hosted capture call. See the ScreenshotNeo API documentation for request options.

cURL

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

Python

import requests

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

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', image);

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its capture can remove cookie and consent banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server gives Claude, Cursor, and other MCP clients take_screenshot, get_page_info, and capture_pdf tools. You can use custom JavaScript or a click action when a page must be put into a particular state before capture.

A hosted capture can remove common overlays before taking the screenshot.
A hosted capture can remove common overlays before taking the screenshot.

There is a free plan with 1,000 screenshots per month and no card. Paid plans start at $5 for 3,000 screenshots, and every feature is included on every plan.

Create a free ScreenshotNeo account and start with 1,000 screenshots a month at no charge.

Troubleshooting

The exported image shows the default option

Cause: the live selection changed, but the clone still reflects the original HTML attribute. Fix: read value or the selected option’s text immediately before capture and write it to a normal text element. If you must retain the native control, synchronize the cloned option’s selected property and attribute.

The label is empty

Cause: there is no selected index, or the select has no options. Fix: use optional chaining and a fallback, for example select.options[select.selectedIndex]?.text ?? 'None selected'.

The native select looks different

Cause: native controls are painted by the browser and are not guaranteed to match across engines. Fix: render a capture-only label or styled replacement for the exported image.

Images or fonts are missing

Cause: dom-to-image must load and embed image and font resources. Cross-origin restrictions or failed requests can prevent rendering, and a tainted canvas can cause rejection. Fix: serve assets with appropriate origin permissions, wait until fonts and images finish loading, enable cacheBust when stale resources are suspected, and handle the rejected promise.

The promise rejects in Safari or another browser

Cause: the renderer relies on SVG foreignObject behavior and browser support varies. The project’s compatibility notes are historical, mention Chrome and Firefox versions available at the time, exclude Internet Explorer, and record a Safari limitation. Fix: test the exact browser and package version you support, or use a server-side browser capture for consistent output.

Changes made just before capture are not visible

Cause: layout, fonts, or images have not settled. Fix: update the label, wait for the next animation frame, and ensure required resources are loaded before calling dom-to-image.

syncCaptureLabel();
await document.fonts?.ready;
await new Promise(requestAnimationFrame);
const dataUrl = await domtoimage.toPng(captureArea);

Performance, reliability, and cost

  • Capture the smallest node that contains the result. Full-page trees with many images and complex styles take more memory and time.
  • Prefer toBlob for uploads or downloads to avoid keeping a large base64 data URL in memory.
  • Reuse a synchronized capture label instead of repeatedly rebuilding large DOM subtrees.
  • Wait for fonts and images once, then capture. Repeated resource loading is a common source of slow or inconsistent output.
  • Use a stable browser and package version in automated jobs, and compare output at the viewport sizes that matter.
  • Client-side dom-to-image has no hosted per-shot charge, but it consumes the user’s browser CPU and depends on browser security rules. A hosted API adds request cost and removes browser setup. ScreenshotNeo bills only clean shots; failed loads, bot checks, blank pages, timeouts, and cache hits are free.

Alternatives and when to use them

html-to-image describes itself as a fork of dom-to-image. html2canvas reconstructs an image from the DOM and styles rather than taking a literal browser screenshot. Compare the target browser, native-control rendering, CSS coverage, asset and cross-origin behavior, output formats, and maintenance status before switching. Any library still needs a test for the exact select, browser, and styling combination in your application.

FAQ

Should I capture select.value or the option text?

Use value when the image should show the submitted or machine-readable value. Use the selected option’s text when readers should see the label.

Can dom-to-image capture an open dropdown menu?

Do not depend on the browser’s native popup menu being part of the DOM node. Render the chosen value in the page and capture that content.

Does changing the select automatically update the HTML?

No. The live selected state and the original selected attribute are different pieces of state. Synchronize a capture representation yourself.

Can I export several selected options?

Yes. Iterate over selectedOptions, format the labels or values, and place the resulting text or list inside the capture node.

When should I use a hosted screenshot API?

Use one when capture must run outside a user’s browser, across many URLs, or with consistent browser setup. ScreenshotNeo also provides PDF output, bulk capture, signed links, async jobs, custom JavaScript, and an MCP server for AI agents.