ScreenshotNeo

BlogHow-to

How to Filter Elements by Class or ID Before Capturing with dom-to-image

Use dom-to-image’s node filter callback to exclude elements by class, ID, or both before rendering a screenshot.

By the ScreenshotNeo team1 October 20267 min read

To exclude elements from a dom-to-image capture, pass a filter function in the options object. The callback receives each DOM node: return true to include it and false to exclude it. Test classList for a class, compare id for an ID, or combine both rules.

The filter contract is documented in the official dom-to-image README. Excluding a node also excludes its children, and the callback is not called for the root node passed to the capture method.

Basic class and ID filter

<div id="capture-root">
  <h1>Invoice</h1>
  <p class="customer-name">Ada Lovelace</p>

  <aside class="exclude-from-capture">
    Export controls and internal notes
  </aside>

  <div id="exclude-from-capture">
    This panel is also omitted
  </div>
</div>

<script type="module">
  import domtoimage from 'dom-to-image';

  const root = document.getElementById('capture-root');

  function filter(node) {
    // The callback can receive non-Element nodes.
    if (node.nodeType !== 1) return true;

    return !node.classList.contains('exclude-from-capture') &&
           node.id !== 'exclude-from-capture';
  }

  domtoimage.toPng(root, { filter })
    .then((dataUrl) => {
      const image = new Image();
      image.src = dataUrl;
      document.body.appendChild(image);
    })
    .catch((error) => console.error('Capture failed:', error));
</script>

A filter is a predicate, not a selector string. The function above includes ordinary nodes and removes any element with the class exclude-from-capture or the ID exclude-from-capture.

Filter by class

const root = document.getElementById('capture-root');

const filter = (node) =>
  node.nodeType !== 1 || !node.classList.contains('no-capture');

domtoimage.toPng(root, { filter })
  .then((dataUrl) => {
    const link = document.createElement('a');
    link.download = 'filtered.png';
    link.href = dataUrl;
    link.click();
  });

classList.contains() matches one class token. It does not perform a substring match, so a class such as no-capture-toolbar is not removed by a check for no-capture.

Filter by ID

const filter = (node) =>
  node.nodeType !== 1 || node.id !== 'no-capture';

domtoimage.toPng(document.getElementById('capture-root'), { filter })
  .then((dataUrl) => console.log(dataUrl));

IDs should be unique in valid HTML. If several elements need excluding, use a class or keep a set of IDs:

const excludedIds = new Set(['debug-panel', 'export-toolbar']);

const filter = (node) =>
  node.nodeType !== 1 || !excludedIds.has(node.id);

Combine classes, IDs, and other rules

const excludedIds = new Set(['debug-panel', 'export-toolbar']);

function filter(node) {
  if (node.nodeType !== 1) return true;

  const hasExcludedClass = node.classList.contains('no-capture');
  const hasExcludedId = excludedIds.has(node.id);
  const isInteractiveControl =
    node.matches('button, input, select, textarea');

  return !hasExcludedClass && !hasExcludedId && !isInteractiveControl;
}

Keep the predicate cheap. It runs while dom-to-image walks the target subtree, so avoid layout reads, network requests, or expensive selector work inside it.

Important filter behavior

Returning false removes the entire subtree

If the callback returns false for an element, that element and all of its descendants are excluded. You do not need to match each child separately.

<section class="no-capture">
  <h2>Everything here disappears</h2>
  <p>This paragraph is removed with its parent.</p>
</section>

The root node is not passed to the callback

The root supplied to toPng, toJpeg, toSvg, toBlob, or toPixelData is always the capture target. The filter is not called for that root, so putting the excluded class or ID on the root will not remove it.

Choose a wrapper as the root when the element you want to omit is currently the root:

<div id="page-shell">
  <div id="temporary-widget">Do not include this</div>
  <main id="capture-root">Capture this content</main>
</div>

<script type="module">
  const root = document.getElementById('page-shell');
  const filter = (node) =>
    node.nodeType !== 1 || node.id !== 'temporary-widget';

  domtoimage.toPng(root, { filter });
</script>

Ancestors must remain included

A descendant can be filtered only if its ancestors are included. If you exclude a parent, every child disappears regardless of the child’s own filter result.

Choosing the capture method and output

The same filter option can be used with dom-to-image’s rendering methods. Each method returns a promise:

Method Result Typical use
toPng PNG data URL Lossless screenshots and downloads
toJpeg JPEG data URL Smaller photographic output
toSvg SVG data URL Inspecting the generated markup
toBlob Blob Uploading or saving without a data URL
toPixelData Pixel array Image analysis or custom encoding
const options = { filter };

const pngUrl = await domtoimage.toPng(root, options);
const jpegUrl = await domtoimage.toJpeg(root, options, { quality: 0.9 });
const blob = await domtoimage.toBlob(root, options);
const pixels = await domtoimage.toPixelData(root, options);

Check the API for the exact package version installed. The similarly named dom-to-image-more fork documents additional controls such as filterStyles; those fork-specific options are not evidence that the original dom-to-image package supports them.

Complete browser example with download

<!doctype html>
<html lang="en">
  <body>
    <button id="capture-button">Download screenshot</button>

    <article id="capture-root">
      <h1>Quarterly report</h1>
      <p>Revenue increased during the quarter.</p>
      <div class="no-capture">Internal annotation</div>
      <div id="debug-panel">Debug information</div>
    </article>

    <script type="module">
      import domtoimage from 'dom-to-image';

      const root = document.getElementById('capture-root');
      const button = document.getElementById('capture-button');
      const excludedIds = new Set(['debug-panel']);

      function filter(node) {
        if (node.nodeType !== 1) return true;
        return !node.classList.contains('no-capture') &&
               !excludedIds.has(node.id);
      }

      button.addEventListener('click', async () => {
        button.disabled = true;
        try {
          const dataUrl = await domtoimage.toPng(root, { filter });
          const link = document.createElement('a');
          link.download = 'quarterly-report.png';
          link.href = dataUrl;
          link.click();
        } catch (error) {
          console.error('Capture failed:', error);
          alert('The screenshot could not be created. Check the console.');
        } finally {
          button.disabled = false;
        }
      });
    </script>
  </body>
</html>

Common mistakes and edge cases

Symptom Cause Fix
The excluded root still appears The root is never passed to filter. Capture a parent wrapper and filter the former root, or choose a different capture root.
Children are unexpectedly missing A parent returned false. Move the class or ID to the smallest subtree that should disappear.
classList throws an error The callback received a non-Element node. Guard with node.nodeType !== 1 before using element properties.
A class rule does not match The class name is being treated as a substring or contains a typo. Use classList.contains('exact-token') and inspect element.className.
An ID rule removes the wrong content Duplicate IDs exist. Make IDs unique or use a shared exclusion class.
The output is blank or incomplete Rendering failed for another reason, such as unsupported resources or a rejected promise. Attach .catch(), inspect the error, and reduce the capture to a minimal subtree.
Changes are not reflected The DOM was captured before the class or ID was added, or before the UI settled. Apply mutations first, then await the capture call in the next step.

Debugging checklist

  1. Verify that root is the element you intend to capture.
  2. Log the node and decision temporarily:
function filter(node) {
  if (node.nodeType !== 1) return true;
  const include = !node.classList.contains('no-capture') &&
                  node.id !== 'debug-panel';
  console.debug(node, include ? 'include' : 'exclude');
  return include;
}
  1. Confirm that the unwanted element is a descendant of the root.
  2. Confirm that no ancestor is being excluded accidentally.
  3. Catch the returned promise and inspect the browser console.
  4. Try toSvg first when you need to inspect the generated result.

Performance and reliability considerations

Filtering reduces the amount of DOM that must be cloned and rendered, especially when a large widget subtree is removed. Keep the callback deterministic and synchronous. Do not mutate the document from inside the callback, because changing the tree while it is being traversed can produce confusing output.

For reliable captures, wait until the target has its final dimensions and content. Images and fonts should be available before calling dom-to-image. If external resources are blocked by browser security policy, the filter cannot fix that; resolve the resource or origin configuration separately.

For repeated captures, reuse a predicate function and remove transient controls before capture when possible. Restore any temporary DOM changes after the promise settles, including on errors.

Or skip the browser setup

If you need a server-side screenshot instead of wiring dom-to-image into a browser, ScreenshotNeo provides a single GET request for a PNG, JPEG, WebP, or PDF. Its API can hide selectors, wait for a selector or network idle, run custom JavaScript, and capture a full page or one CSS-selected element. See the ScreenshotNeo documentation for the full option list.

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('fs').writeFileSync('shot.webp', image);

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and whether the shot was billed. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 shots.

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

FAQ

Can I pass a CSS selector directly as the filter?

The documented interface expects a callback that receives a DOM node. Use node.matches(), classList.contains(), or an ID comparison inside that callback.

Does filtering change the live page?

No. The predicate controls what dom-to-image includes in the rendered clone. It does not remove the matching element from the document.

Can I exclude several classes?

Yes. Test multiple classList.contains() calls, or store class names in a set and check them with Array.from(node.classList).

Why does excluding a parent remove content I wanted?

Returning false excludes the node and its complete subtree. Apply the exclusion to a narrower descendant when only part of the section should be omitted.