Recommended Free Tools
Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
To save one screenshot per element ID in PhantomJS, open the page, collect each element’s bounding rectangle inside page.evaluate(), assign that rectangle to page.clipRect, and call page.render() with a unique filename. The complete script below checks loading status, skips missing or zero-size elements, and exits only after all renders are requested.
The complete PhantomJS script
Save this as capture-elements.js. Replace the URL and the ids array with your page and target elements, then run it with the PhantomJS executable available in your environment.
var page = require('webpage').create();
var address = 'https://example.com/';
var ids = ['header', 'main', 'footer'];
page.open(address, function (status) {
if (status !== 'success') {
console.log('Unable to load ' + address);
phantom.exit(1);
return;
}
var boxes = page.evaluate(function (elementIds) {
return elementIds.map(function (id) {
var element = document.getElementById(id);
if (!element) {
return { id: id, missing: true };
}
var rect = element.getBoundingClientRect();
return {
id: id,
top: rect.top + window.pageYOffset,
left: rect.left + window.pageXOffset,
width: rect.width,
height: rect.height
};
});
}, ids);
boxes.forEach(function (box) {
if (box.missing || box.width <= 0 || box.height <= 0) {
console.log('Skipping missing or empty element: ' + box.id);
return;
}
page.clipRect = {
top: box.top,
left: box.left,
width: box.width,
height: box.height
};
page.render(box.id + '.png');
});
phantom.exit();
});
The output is one PNG per successfully measured ID, such as header.png, main.png, and footer.png. PhantomJS’s quick start describes it as a command-line tool; the page-opening callback reports a load status, while phantom.exit() ends the process when the work is complete.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →How the boundary between page code and PhantomJS code works
Run DOM code inside page.evaluate()
document.getElementById(), getBoundingClientRect(), window.pageXOffset, and other browser APIs are available in the page context. They are not available in the outer PhantomJS script. The callback therefore returns ordinary data—strings, numbers, booleans, arrays, and objects—rather than DOM nodes.
#1 Best Overall
Return serializable values only
page.evaluate() is sandboxed. A DOM element, a function, or an object containing browser-only references cannot be used as the result in the outer script. Returning the rectangle’s numeric fields is safe and keeps the rendering loop simple.
Why scroll offsets are added
getBoundingClientRect() reports coordinates relative to the viewport. Adding window.pageYOffset and window.pageXOffset converts them to page coordinates for the clip rectangle. Keep the viewport and clipping coordinates consistent, especially on pages that scroll, use transforms, or change layout at different viewport widths.
Set a predictable viewport before measuring
Responsive CSS can produce different boxes at different widths. Set page.viewportSize before page.open() when a fixed capture size matters:
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemspage.viewportSize = {
width: 1440,
height: 900
};
page.open(address, function (status) {
// measure and render here
});
The viewport controls layout; clipRect controls which region is written to the image. A tall element can extend below the viewport, but verify the result on the PhantomJS version you use when clipping content outside the initially visible area.
Make filenames safe and deterministic
The basic example uses the ID as the filename. If IDs come from outside your script, sanitize them so a slash, backslash, or other path character cannot redirect output. A counter also prevents collisions when a list accidentally contains the same ID twice:
Rank #2
function fileNameFor(box, index) {
var safe = box.id.replace(/[^a-zA-Z0-9_-]/g, '_');
return (index + 1) + '-' + safe + '.png';
}
boxes.forEach(function (box, index) {
if (box.missing || box.width <= 0 || box.height <= 0) {
console.log('Skipping missing or empty element: ' + box.id);
return;
}
page.clipRect = {
top: box.top,
left: box.left,
width: box.width,
height: box.height
};
page.render(fileNameFor(box, index));
});
Use integer dimensions if your target build produces rounding artifacts. For high-density output, test the exact PhantomJS build and image viewer you depend on; the documented API exposes viewport and clipping dimensions, but device-pixel behavior can vary between builds.
Capture only the IDs you actually need
Known IDs
getElementById() is the clearest choice when the caller already supplies an ID list. The function returns one node or null, so the null check in the main script prevents a failed rectangle read.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchCSS selectors instead of IDs
If the input is a selector, pass that selector as a serializable argument and resolve it in the page context:
var selector = '.card[data-state="ready"]';
var boxes = page.evaluate(function (css) {
var nodes = document.querySelectorAll(css);
var result = [];
for (var i = 0; i < nodes.length; i += 1) {
var rect = nodes[i].getBoundingClientRect();
result.push({
index: i,
top: rect.top + window.pageYOffset,
left: rect.left + window.pageXOffset,
width: rect.width,
height: rect.height
});
}
return result;
}, selector);
Use a unique index in the output filename because several nodes can match one selector. The same clipRect and page.render() loop then produces one image per match.
Duplicate IDs
HTML is intended to use an ID once. getElementById() returns a single element when duplicates exist, so duplicate markup requires a selector-based strategy and an explicit rule for which matches to keep.
Rank #3
Wait for dynamic content before measuring
The page.open() callback tells you that the navigation completed with a success or fail status. Modern pages may still insert cards, images, or navigation after that callback. Measuring immediately can capture an empty box or the wrong height.
There is no universal wait value that works for every site. If the page exposes a reliable application flag, poll for it; otherwise, use a bounded delay and document the assumption:
function waitForReady(done, remaining) {
var ready = page.evaluate(function () {
return document.body && document.body.getAttribute('data-render-ready') === 'true';
});
if (ready || remaining <= 0) {
done();
return;
}
window.setTimeout(function () {
waitForReady(done, remaining - 1);
}, 250);
}
page.open(address, function (status) {
if (status !== 'success') {
phantom.exit(1);
return;
}
waitForReady(function () {
// run the evaluate/clipRect/render loop here
}, 40);
});
This pattern waits up to about 10 seconds for a page-owned readiness attribute. Replace it with a condition that your page actually sets, or use a measured delay when no such signal exists. Test pages with lazy images, animations, delayed fonts, and content inside frames separately.
Render one element or one combined region?
Separate files
Assign a new clipRect and call page.render() for every box when downstream processing expects one file per element. Always use distinct names; each render writes the current clip region.
One larger image
To capture a combined region, calculate a bounding rectangle that contains all desired elements, assign it once, and render once. A later render with a different clip rectangle does not append to the first image—it replaces the output file if you reuse its name.
Choose an output format
PNG is a practical default for UI screenshots because it preserves sharp text and transparency where the build supports it. The capture API also documents JPEG, GIF, and PDF output. Confirm the exact formats accepted by the PhantomJS binary you deploy, and choose a filename extension that matches the requested format.
Run and verify the job
- Put the script in a writable working directory.
- Set
address, the ID list, and, when needed,page.viewportSize. - Run
phantomjs capture-elements.js. - Check the console for load failures and skipped IDs.
- Open every output file and verify that the crop contains the intended element rather than a blank or shifted region.
Do not treat a process that exits cleanly as proof that every capture succeeded: the script intentionally skips missing and zero-size boxes, and a page can return a successful navigation status while still rendering incomplete application content.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Unable to load is printed |
page.open() returned a status other than success. |
Check the URL, DNS, TLS requirements, redirects, and network access available to the PhantomJS process. Exit with a nonzero status, as the example does, rather than rendering. |
| An ID is skipped | getElementById() returned null. |
Inspect the exact spelling and case, confirm the element exists in the loaded document, and move measurement after the code that inserts it. |
| An image is skipped despite the element existing | The rectangle has zero width or height because it is hidden, collapsed, or not laid out yet. | Wait for the relevant state, remove the hidden style for the capture, or choose a visible ancestor. Log the returned dimensions to distinguish timing from CSS. |
| The crop is shifted | Viewport-relative coordinates were used as page coordinates, or the page scrolled between measurement and rendering. | Add the page scroll offsets as shown, keep scrolling stable, and retest pages with fixed headers, transforms, and nested scrolling containers. |
| The crop has the wrong responsive layout | The viewport was not set, or it differs from the environment used to design the page. | Assign page.viewportSize before opening the page and use the same dimensions for every run. |
| Dynamic text or images are missing | The navigation callback fired before asynchronous rendering finished. | Wait for a page-specific readiness signal or a bounded delay, then measure. There is no single delay that is reliable for all sites. |
| Several captures overwrite one another | The same output filename was reused. | Include the ID, a sanitized value, or a match index in every filename. |
| Frames are not captured correctly | The target lives in a nested browsing context with its own document and coordinate system. | Inspect the frame document separately and verify coordinate conversion for the PhantomJS version in use; do not assume top-level page coordinates apply. |
| Output format errors occur | The selected PhantomJS build does not support the requested format or extension combination. | Start with PNG, then confirm the build’s documented format support before switching to JPEG, GIF, or PDF. |
Performance and reliability considerations
Measure once, render in a loop
The script gathers all rectangles in one page-context call and then renders each region. That avoids repeatedly crossing the page/outer-script boundary. If the page changes layout during the loop, measure and render in a controlled state or capture immediately after a readiness condition.
Keep the target list bounded
Each render produces a separate file and consumes I/O. For large lists, process IDs in batches, use deterministic names, and record skipped items so a later run can retry only failures.
Free tools Windows power users keep installed
One-click scans. No signup required.
Control animations and lazy loading
Animations can change a rectangle between measurement and rendering. Disable them with page-specific CSS or wait for a stable state. Lazy images may need scrolling or an application-provided “loaded” signal before their final dimensions are available.
Protect credentials and private pages
Do not place session tokens or passwords in filenames, console output, or source control. If authentication is required, establish it through the page’s supported mechanism and make sure saved screenshots do not expose private data to an unintended directory.
Or skip the browser setup
ScreenshotNeo is a hosted website screenshot API and MCP server. Its element capture option can target one element by CSS selector, while custom JavaScript, waits, viewport and device presets, retina scale, full-page capture, dark mode, cookies, headers, user agents, authorization, timezone, geolocation, hidden selectors, blocked ads or trackers, resource blocking, transparent backgrounds, resizing, caching with a chosen TTL, signed image links, asynchronous jobs, webhooks, bulk capture of up to 100 URLs per call, usage data, and an OpenAPI specification cover the cases that otherwise require browser orchestration. It accepts the parameter names used by other screenshot APIs, which can simplify a migration.
Before capture, it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.
For a one-call image, see the ScreenshotNeo API documentation and use:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
The equivalent Python request is:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
In Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients, so an AI agent can request captures without you wiring PhantomJS callbacks. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account to get started.
PhantomJS or ScreenshotNeo for this job?
| Choose | Best fit | Trade-off |
|---|---|---|
| PhantomJS script | You need local files, deterministic custom code, or an existing PhantomJS pipeline. | You must manage the executable, page timing, viewport behavior, clipping coordinates, output storage, and failures. |
| ScreenshotNeo | You want an HTTP or MCP workflow, selector-based element capture, cleanup of overlays, and billing that excludes failed or unusable page loads. | You send the URL and options to a hosted service and use an API key. |
For a known list of IDs on a page you control, the PhantomJS loop is direct and transparent. For repeated captures across sites, automated agents, or pages covered by consent banners and widgets, the hosted call removes much of the browser setup.
Frequently Asked Questions
Can I return an element itself from page.evaluate()?
No. Return serializable data such as its ID, text, or rectangle fields, then use those values in the outer PhantomJS script.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Why does a successful page.open() not guarantee a complete screenshot?
The navigation callback reports load status, but application code can continue inserting or resizing content afterward. Wait for a page-specific readiness condition before measuring.
How do I capture an element inside an iframe?
The iframe has a separate document and coordinate system. Resolve the target in that frame, convert its coordinates relative to the top-level page, and verify the result on the PhantomJS build you deploy.
Can one render call produce separate files for several IDs?
No. Set one clip rectangle and filename per render for separate images; a single render captures only its current region.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

