ScreenshotNeo

BlogHow-to

Crop Screenshot to an Element in PhantomJS

Measure a DOM element, set PhantomJS clipRect, and render a precise crop with fixes for scrolling, async content, and legacy runtime limits.

By the ScreenshotNeo team1 October 20266 min read

Direct answer: find the element in the page context, read its rectangle with getBoundingClientRect(), convert viewport coordinates to page coordinates by adding scroll offsets, assign the numeric object to page.clipRect, then call page.render(). clipRect is the rectangle PhantomJS rasterizes. Measure only after the element and its visual state are ready.

This is maintenance guidance for existing PhantomJS scripts. The upstream project is suspended and its repository has been read-only since May 30, 2023; version 2.1 is the latest stable release. Verify your installed binary and target pages before depending on this workflow.

Minimal working script

Save as crop-element.js and run phantomjs crop-element.js. Change the URL and selector.

var page = require('webpage').create();
page.viewportSize = { width: 1280, height: 900 };

page.open('https://example.com/', function (status) {
  if (status !== 'success') {
    console.log('Unable to load the page');
    phantom.exit(1);
    return;
  }

  var rect = page.evaluate(function (selector) {
    var element = document.querySelector(selector);
    if (!element) return null;
    var box = element.getBoundingClientRect();
    return {
      top: box.top + window.pageYOffset,
      left: box.left + window.pageXOffset,
      width: box.width,
      height: box.height
    };
  }, '#target');

  if (!rect || rect.width <= 0 || rect.height <= 0) {
    console.log('Target element not found or has no visible dimensions');
    phantom.exit(1);
    return;
  }

  page.clipRect = rect;
  page.render('element.png');
  phantom.exit();
});

The official clipRect property defines the rectangular area rasterized by page.render. page.evaluate runs the measurement in the page context and returns JSON-serializable values, so return numbers in a plain object rather than a DOM node.

How the crop works

  1. Choose the layout. Set viewportSize before navigation. Both width and height are required, and responsive CSS uses these values.
  2. Wait for readiness. page.open means the navigation completed, not that client-side rendering, fonts, images, or data requests have finished.
  3. Measure in the DOM. getBoundingClientRect() returns viewport-relative coordinates. Add pageXOffset and pageYOffset for page coordinates.
  4. Validate. Handle a missing selector, zero dimensions, and unusual transformed or off-screen elements before setting the clip.
  5. Render. Assign {top,left,width,height} to page.clipRect immediately before page.render.

Waiting for asynchronous content

For a page that adds the target after load, poll for a condition instead of measuring immediately. A bounded poll avoids hanging forever:

function waitForTarget(selector, deadline, done) {
  var found = page.evaluate(function (s) {
    var el = document.querySelector(s);
    return !!el && el.getBoundingClientRect().width > 0 && el.getBoundingClientRect().height > 0;
  }, selector);
  if (found) return done(true);
  if (Date.now() >= deadline) return done(false);
  window.setTimeout(function () { waitForTarget(selector, deadline, done); }, 100);
}

waitForTarget('#target', Date.now() + 10000, function (ready) {
  if (!ready) {
    console.log('Timed out waiting for target');
    phantom.exit(1);
    return;
  }
  // Measure, set page.clipRect, and render here.
});

A known delay can work for a static application, but a condition is safer because network and JavaScript timing vary.

Reusable version with arguments and output formats

var system = require('system');
var webpage = require('webpage');
var page = webpage.create();
var args = system.args;
var url = args[1] || 'https://example.com/';
var selector = args[2] || '#target';
var output = args[3] || 'element.png';

page.viewportSize = { width: 1280, height: 900 };
page.open(url, function (status) {
  if (status !== 'success') {
    console.log('Unable to load ' + url);
    phantom.exit(1);
    return;
  }
  var rect = page.evaluate(function (s) {
    var el = document.querySelector(s);
    if (!el) return null;
    var b = el.getBoundingClientRect();
    return { top: b.top + pageYOffset, left: b.left + pageXOffset,
             width: b.width, height: b.height };
  }, selector);
  if (!rect || rect.width <= 0 || rect.height <= 0) {
    console.log('No visible element matched ' + selector);
    phantom.exit(1);
    return;
  }
  page.clipRect = rect;
  page.render(output);
  console.log(JSON.stringify(rect));
  phantom.exit();
});

The render API chooses the format from the filename. PNG preserves UI detail without lossy compression; JPEG is smaller but lossy. The documented formats include PNG, JPEG, BMP, PPM and PDF (GIF depends on the Qt build). Use an extension your installed build supports.

Selectors, scrolling, and visual edge cases

  • Selector syntax: querySelector accepts CSS selectors. Escape special characters and scope to a stable container when class names are generated.
  • Multiple matches: querySelector returns the first match. Use querySelectorAll and choose an index or compute a union when you need several elements.
  • Scrolled pages: the scroll-offset addition converts viewport coordinates to document coordinates. Validate this on your PhantomJS build when the page is scrolled or the element is position-fixed.
  • Fixed and transformed elements: CSS transforms, sticky positioning, and fractional pixels can produce a rectangle that differs from the painted pixels. Test the output and, if necessary, capture at the intended scroll position.
  • Overflow and clipping: an element can have a large layout box while an ancestor hides part of it. clipRect crops the rectangle; it does not change CSS overflow.
  • Lazy images and fonts: wait until image dimensions are nonzero and fonts have settled, otherwise the measured box or pixels may shift after capture.
  • Device scale: PhantomJS raster dimensions follow the page and renderer behavior of the installed build; do not assume modern device-pixel-ratio semantics.

Debugging checklist

Symptom Likely cause Fix
“Target element not found” Wrong selector or content is injected later Log document.documentElement.innerHTML temporarily, verify the selector, and poll for the element.
Zero-size or blank image Hidden element, collapsed parent, or measurement before layout Check computed visibility and dimensions; wait for data, images, and CSS; reject non-positive width/height.
Crop is shifted after scrolling Viewport coordinates were used as page coordinates Add pageXOffset/pageYOffset as shown, then validate fixed-position elements.
Only part of a card appears Ancestor overflow or a transformed child Inspect overflow and transforms; capture the visible container or adjust page state before measuring.
Old content in the shot SPA/API request still running Wait on a selector or application-ready flag rather than a short fixed delay.
Navigation fails TLS, DNS, redirect, or incompatible site features Check the status and PhantomJS console errors, test the URL in the same runtime, and provide a fallback for unsupported pages.
Output format error Unsupported extension or build-specific codec Start with PNG; consult the installed render documentation before using JPEG, PDF, or GIF.

Performance and reliability

  • Reuse one PhantomJS process for a batch only when page state is reset between URLs; otherwise isolate jobs to avoid cookies and DOM state leaking.
  • Keep the viewport no larger than the layout you need. Large pages and full-resolution images increase rasterization time and memory.
  • Measure once after readiness, then render once. Repeated DOM queries and renders add work without improving the crop.
  • Set an external process timeout and capture PhantomJS exit status. A page can remain technically open while its application is unusable.
  • Record URL, selector, viewport, scroll position, output format, and rectangle with each artifact so failures are reproducible.
  • Because PhantomJS is unmaintained, pin the binary and operating-system image, and keep a migration path to a maintained browser for new work.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API when you do not want to maintain a PhantomJS runtime. It supports element capture by CSS selector, full-page shots, custom viewports, and PNG, JPEG, or WebP output. Before capture it accepts consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.

See the ScreenshotNeo API docs for all options. A one-call capture looks like this:

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)
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}`);

ScreenshotNeo also has an MCP server with 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.

FAQ

Does clipRect crop the DOM or only the output?

It limits the rectangle rasterized by page.render; it does not alter the document or CSS layout.

Can I return the element from page.evaluate?

No. Return simple JSON-serializable numbers and strings, such as the rectangle object.

Why is my element screenshot different on another machine?

Viewport, fonts, network timing, PhantomJS/Qt build, and page state all affect layout and pixels. Pin the runtime and wait for readiness.

Should new projects still use PhantomJS?

PhantomJS is suspended. Use this recipe to maintain an existing script and evaluate a maintained browser or ScreenshotNeo for new automation.