ScreenshotNeo

BlogHow-to

Fix a Puppeteer Screenshot That Captures an Empty SVG or Canvas Area

Diagnose empty SVG and canvas captures by checking bounds, dimensions, asset loading, and application render readiness before taking a Puppeteer screenshot.

By the ScreenshotNeo team4 October 20267 min read

An empty SVG or canvas in a Puppeteer screenshot usually comes down to one of two things: the element is outside the captured region, or the application has not finished drawing when the screenshot is taken. Check the element’s bounds and dimensions first, then wait for an application-specific render-complete signal. A loaded page, visible element, or stable layout does not by itself guarantee that canvas pixels have been painted.

1. Check the target and captured area

First confirm that Puppeteer is on the expected page and frame, then inspect the element. It should exist, be visible, have nonzero bounds, and fall inside the area you capture. Review the viewport and screenshot options, especially clip, fullPage, and captureBeyondViewport.

const selector = '#chart';
await page.waitForSelector(selector, { visible: true });
const box = await page.$eval(selector, element => {
  const rect = element.getBoundingClientRect();
  const style = getComputedStyle(element);
  return {
    tag: element.tagName,
    width: rect.width,
    height: rect.height,
    x: rect.x,
    y: rect.y,
    display: style.display,
    visibility: style.visibility,
    opacity: style.opacity
  };
});
console.log(box);

A zero-width or zero-height box points to layout or sizing. A nonzero box outside the viewport or clip points to capture geometry. Also check whether another element covers the target and look for console errors or failed asset requests.

Puppeteer can capture the whole page with page.screenshot() or a specific element with ElementHandle.screenshot(). Element screenshots scroll the target into view when needed. Compare both forms: if the element capture works but the page capture does not, focus on viewport, clip, and full-page settings.

2. Inspect SVG and canvas dimensions

SVG

For inline SVG, inspect its rendered box and its viewBox, width, and height attributes. A viewBox defines a coordinate system; it does not alone guarantee a visible rendered size. If the SVG is loaded as an image asset and drawn into canvas, give the root <svg> explicit width and height and wait for its image load before drawing.

Canvas

Canvas has intrinsic pixel dimensions in width and height, separate from CSS display dimensions. Check both those intrinsic dimensions and the rendered bounding box. A canvas with zero dimensions, or dimensions beyond the browser’s maximum canvas size, can return data:, from toDataURL(). Reading a canvas that is not origin-clean throws a SecurityError.

const canvasInfo = await page.$eval('canvas', canvas => {
  const rect = canvas.getBoundingClientRect();
  let dataUrlStatus;
  try {
    const result = canvas.toDataURL();
    dataUrlStatus = result === 'data:,' ? 'empty-or-too-large' : 'readable';
  } catch (error) {
    dataUrlStatus = `${error.name}: ${error.message}`;
  }
  return {
    width: canvas.width,
    height: canvas.height,
    renderedWidth: rect.width,
    renderedHeight: rect.height,
    dataUrlStatus
  };
});
console.log(canvasInfo);

Do not print a large data URL to logs. The diagnostic only needs to distinguish a readable canvas from a zero/oversized one or a security exception.

3. Wait for the application to finish drawing

Navigation completion and visual completion are separate. waitForSelector confirms presence or visibility; locator stability concerns layout. Neither knows when your charting library or application has completed canvas drawing. The most reliable condition is one the page sets after rendering finishes, such as a flag or a rendered-state attribute.

await page.goto(url, { waitUntil: 'networkidle2' });
await page.waitForSelector('#chart', { visible: true });
await page.waitForFunction(() => window.chartRenderComplete === true);
await page.screenshot({ path: 'capture.png' });

networkidle2 can be useful as a navigation condition, but it is not proof that later client-side drawing has finished. The example assumes the application sets window.chartRenderComplete only after it has painted the chart; replace it with the real signal exposed by your app. If you cannot change the page, wait for a meaningful DOM state or observable application condition. A fixed delay can mask a race on a fast machine and fail on a slow one.

4. Wait for image assets before drawing them

If your code calls drawImage() with an image or SVG asset, wait for its load event first. Calling drawImage() before the image has loaded draws nothing.

await page.evaluate(async () => {
  const image = document.querySelector('#chart-image');
  if (!image.complete) {
    await new Promise((resolve, reject) => {
      image.addEventListener('load', resolve, { once: true });
      image.addEventListener('error', reject, { once: true });
    });
  }
  if (image.naturalWidth === 0) throw new Error('Image has no decoded dimensions');
  const canvas = document.querySelector('canvas');
  const context = canvas.getContext('2d');
  context.drawImage(image, 0, 0);
});

Adapt this to the application’s rendering code. If drawing is asynchronous, the page should signal completion after the draw operation rather than immediately after starting it.

5. Capture the right region

Without a clip, Puppeteer’s screenshot options include fullPage: false and captureBeyondViewport: false. A target below the viewport may therefore be absent from a viewport screenshot. Use an element screenshot when appropriate, scroll it into view, or set page screenshot bounds deliberately.

const chart = await page.$('#chart');
if (!chart) throw new Error('Chart element not found');
await chart.screenshot({ path: 'chart.png' });

// Alternatively, capture the whole document:
await page.screenshot({ path: 'full-page.png', fullPage: true });

When using clip, make sure its coordinates and dimensions cover the target. A clip is a capture boundary, not a request to move the element. For a diagnosis, take one full-page or viewport screenshot and one element screenshot, then compare.

6. Complete diagnostic example

This runnable pattern collects the key checks, waits for a page-defined render signal, and captures the target. Set TARGET_URL, selector, and readiness condition for your application.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
    page.on('console', message => console.log('PAGE:', message.type(), message.text()));
    page.on('pageerror', error => console.error('PAGE ERROR:', error.message));
    page.on('requestfailed', request =>
      console.error('REQUEST FAILED:', request.url(), request.failure()?.errorText));

    await page.goto(process.env.TARGET_URL, { waitUntil: 'networkidle2', timeout: 60000 });
    const selector = '#chart';
    await page.waitForSelector(selector, { visible: true, timeout: 15000 });

    // The application must set this only after its SVG/canvas drawing is complete.
    await page.waitForFunction(() => window.chartRenderComplete === true, { timeout: 30000 });

    const diagnostic = await page.$eval(selector, element => {
      const rect = element.getBoundingClientRect();
      const result = {
        tag: element.tagName,
        width: rect.width,
        height: rect.height,
        x: rect.x,
        y: rect.y
      };
      if (element instanceof HTMLCanvasElement) {
        result.canvasWidth = element.width;
        result.canvasHeight = element.height;
        try { result.dataUrl = element.toDataURL() === 'data:,' ? 'data:,' : 'readable'; }
        catch (error) { result.dataUrl = `${error.name}: ${error.message}`; }
      }
      return result;
    });
    console.log(diagnostic);

    const target = await page.$(selector);
    await target.screenshot({ path: 'target.png' });
    await page.screenshot({ path: 'page.png' });
  } finally {
    await browser.close();
  }
})().catch(error => { console.error(error); process.exitCode = 1; });

The readiness flag is illustrative; without an application signal, this example will time out rather than falsely treating element visibility as completed drawing.

7. Troubleshooting

Symptom Likely cause Fix
Element not found Wrong selector, frame, route, or page state Confirm navigation and frame, then inspect the DOM and selector.
Element found but bounds are zero Hidden or unlaid-out content, or missing size styles Wait for visibility/layout and set nonzero dimensions in the app.
SVG box exists but appears blank SVG has no visible paths, a mismatched viewBox, hidden styles, or drawing is still pending Inspect SVG markup, computed styles, viewBox, and app render state.
Canvas returns data:, Zero dimensions or dimensions above the browser canvas limit Set valid intrinsic width and height and avoid oversized canvas dimensions.
toDataURL() throws SecurityError Canvas is not origin-clean, commonly due to cross-origin content without suitable CORS permission Serve assets with appropriate CORS permission or avoid reading/exporting the tainted canvas.
Canvas is blank despite valid dimensions Capture happened before drawing, or image source was not loaded Wait for the app’s completed-render signal and image load before drawImage().
Element screenshot works, page screenshot does not Viewport, clip, or page capture bounds exclude the element Adjust viewport/full-page behavior or capture the element directly.
Intermittent blank captures Race between navigation, asset loading, and client-side drawing Replace arbitrary sleeps with explicit asset and render readiness checks.

8. Performance, reliability, and cost

Waiting for a specific render condition is generally more efficient than sleeping for a generous fixed interval on every capture: it proceeds as soon as the condition is true and gives a useful timeout when it never becomes true. Keep diagnostic listeners and canvas readback checks focused; exporting or logging large pixel data adds memory and logging overhead. Full-page captures can include substantially more content than a target element capture, so choose the smallest region that meets the task.

For reliable automation, make render completion observable in the application, set finite timeouts, log page errors and failed requests, and save both target and page captures during diagnosis. Browser-based capture cost depends on the infrastructure and workload you run; Puppeteer itself does not define a per-screenshot price.

9. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its one-request API can capture a URL without maintaining your own Puppeteer browser:

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

See the ScreenshotNeo API documentation for parameters. Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

Sign up for 1,000 free screenshots a month, with no card.

FAQ

Does networkidle2 guarantee that canvas drawing is finished?

No. It is a navigation wait condition; client-side drawing can continue after network activity settles. Wait for an application-specific completion signal.

Can Puppeteer capture an SVG element directly?

Yes. Capture the page or use an element handle screenshot after confirming the SVG has visible bounds and its rendering is complete.

Why is the canvas visible in the browser but blank in my exported screenshot?

The screenshot may be clipped or taken before drawing. Compare element and page captures, inspect dimensions, and wait for the drawing code to finish.

Should I use a fixed delay?

Use one only when there is no observable readiness signal and you understand its limits. A page-owned completion condition is more reliable across different load times.

Sources