Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Short answer: PhantomJS can move its simulated mouse with page.sendEvent('mousemove', x, y), but the documented page.render() API does not provide an option for drawing an operating-system cursor in the saved image. To show a pointer, add a temporary HTML/CSS cursor overlay before rendering, or composite a cursor graphic onto the finished image afterward.
What PhantomJS actually captures
page.render() renders the page content and saves it as an image (and, where supported by the renderer, a PDF). The output is the browser page, not the desktop, window chrome or a hardware mouse pointer. PhantomJS documentation describes mouse events separately through page.sendEvent(); it does not document a flag that includes an operating-system cursor in a screenshot.
That creates two different jobs:
- Trigger a page state: send a
mousemoveevent so CSS:hoverrules or JavaScript hover handlers can run. - Draw a pointer in the image: put a cursor-shaped element in the DOM before calling
page.render(), or add a graphic after the file is written.
Do not assume that moving the simulated mouse will make a visible arrow appear. It changes the page’s interaction state; it does not establish that PhantomJS paints a system cursor into the output.
Free tools Windows power users keep installed
One-click scans. No signup required.
Method 1: add a cursor overlay inside the page
An in-page overlay is the most controllable approach when the pointer must line up with content such as a button, menu or chart. Because the pointer becomes ordinary page content, page.render() captures it along with everything else.
#1 Best Overall
Complete PhantomJS script
Save this as cursor-shot.js and run it with the PhantomJS executable available on your system. The example places a white, black-outlined arrow at viewport coordinates (320, 240), moves the simulated mouse to the same point, waits briefly for the page to settle, then writes a PNG.
var system = require('system');
var page = require('webpage').create();
var targetUrl = system.args[1] || 'https://example.com';
var outputFile = system.args[2] || 'phantom-with-cursor.png';
var cursorX = 320;
var cursorY = 240;
page.viewportSize = {
width: 1280,
height: 800
};
page.settings.resourceTimeout = 30000;
page.open(targetUrl, function (status) {
if (status !== 'success') {
console.log('Unable to load ' + targetUrl + ' (status: ' + status + ')');
phantom.exit(1);
}
// This can activate CSS :hover rules and JavaScript mouse handlers.
page.sendEvent('mousemove', cursorX, cursorY);
// Add a visible pointer to the document so render() can capture it.
page.evaluate(function (x, y) {
var old = document.getElementById('__phantom_cursor_overlay__');
if (old) {
old.parentNode.removeChild(old);
}
var cursor = document.createElement('div');
cursor.id = '__phantom_cursor_overlay__';
cursor.setAttribute('aria-hidden', 'true');
cursor.style.position = 'fixed';
cursor.style.left = x + 'px';
cursor.style.top = y + 'px';
cursor.style.width = '0';
cursor.style.height = '0';
cursor.style.zIndex = '2147483647';
cursor.style.pointerEvents = 'none';
cursor.style.borderTop = '18px solid #fff';
cursor.style.borderRight = '10px solid transparent';
cursor.style.filter = 'drop-shadow(1px 1px 0 #000)';
document.documentElement.appendChild(cursor);
}, cursorX, cursorY);
window.setTimeout(function () {
page.render(outputFile, {format: 'png', quality: 100});
phantom.exit();
}, 500);
});
Run it with:
phantomjs cursor-shot.js https://example.com example-cursor.png
The CSS shape above is deliberately simple. For a realistic pointer, replace the borders with a background image or an inline SVG encoded as a data URL. Keep the overlay’s pointer-events set to none so it cannot intercept clicks or alter the hover target.
Choosing coordinates and coordinate space
sendEvent() receives viewport coordinates in CSS pixels. The overlay uses position: fixed, so its left and top values refer to that same viewport. If you use position: absolute, scrolling can shift the pointer relative to the visible area. For a full-page render, decide whether the pointer belongs to the initial viewport or to a document position, then account for the scroll offset.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →To point at an element rather than a hard-coded location, calculate its rectangle in the page and pass the result back to PhantomJS:
Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
var point = page.evaluate(function () {
var node = document.querySelector('#checkout');
if (!node) {
return null;
}
var box = node.getBoundingClientRect();
return {
x: Math.round(box.left + box.width / 2),
y: Math.round(box.top + box.height / 2)
};
});
if (point) {
page.sendEvent('mousemove', point.x, point.y);
page.evaluate(function (x, y) {
var cursor = document.getElementById('__phantom_cursor_overlay__');
if (cursor) {
cursor.style.left = x + 'px';
cursor.style.top = y + 'px';
}
}, point.x, point.y);
}
Do this after the target element exists and after any layout-changing scripts have run. If the page is responsive, set page.viewportSize before measuring; changing it later invalidates the rectangle.
Capturing hover states without showing a pointer
If your goal is only to open a menu, reveal a tooltip or activate a CSS hover style, omit the overlay and keep the event:
page.sendEvent('mousemove', 320, 240);
window.setTimeout(function () {
page.render('hover-state.png');
phantom.exit();
}, 300);
The delay gives event handlers and transitions a chance to run. There is no universal delay that guarantees every site has finished; use a page-specific condition when possible, such as checking for a class or visible element in a polling loop.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Method 2: composite a cursor after rendering
Post-capture compositing keeps the web page untouched. First render the screenshot normally, then place a cursor PNG or SVG at the desired pixel coordinates with an image editor or image-processing library. This is useful when:
Rank #3
- the page has strict styling or a restrictive content-security policy;
- you need the exact same pointer artwork on many screenshots;
- the cursor should appear above the browser viewport, including over elements that would otherwise clip an in-page child; or
- you already have an image pipeline that adds annotations, callouts or watermarks.
Use the same viewport dimensions and pixel scale when calculating the composite position. If the screenshot is rendered at a retina scale or resized afterward, scale the cursor coordinates and cursor artwork by the same factor. A composited pointer is not part of the page DOM, so it cannot trigger hover behavior; send mousemove during the PhantomJS capture if you need both effects.
Overlay versus compositing
| Requirement | DOM/CSS overlay | Post-capture compositing |
|---|---|---|
| Pointer must activate hover behavior | Yes, when combined with sendEvent() |
No; hover must be triggered during capture |
| Pointer should be aligned to a live element | Easy to measure with getBoundingClientRect() |
Requires transferring coordinates into image pixels |
| Page must remain visually untouched | No; a temporary node is inserted | Yes |
| Exact, reusable cursor artwork | Possible with CSS, SVG or an image | Usually simplest |
| Works with a separate image pipeline | Not required | Required |
Timing, full-page shots and dynamic pages
Wait for the state you intend to show
Calling render() immediately after page.open() can capture a loading shell, late fonts or an unexpanded menu. Use a callback or polling function that verifies the required DOM state. A short timer is a fallback, not proof that network activity has ended.
Full-page rendering
For a full-page image, PhantomJS examples commonly adjust page.clipRect or the viewport after measuring document dimensions. A fixed-position overlay stays tied to the viewport; an absolute-position overlay follows document coordinates. Decide which behavior you want before changing the page size. If you capture several scroll regions, reposition the overlay for each region or composite it afterward.
Retina and output formats
Keep cursor dimensions proportional to the screenshot’s pixel dimensions. A pointer that is 24 CSS pixels wide may look half-size or double-size if your pipeline later changes the device scale or resizes the image. Render to PNG when you need a crisp pointer with transparency; choose JPEG only when a lossy photograph-style output is acceptable.
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
Troubleshooting
The screenshot has no cursor
- Confirm that the overlay is appended to
document.documentElementordocument.bodybeforepage.render(). - Check that its
z-indexis high and that its border, background or image is not transparent. - Make sure a page script did not remove the temporary node; add it as late as possible, immediately before rendering.
- If you expected the operating-system arrow, switch to the overlay or compositing method;
sendEvent()alone does not document that behavior.
The hover menu does not open
- Send
mousemoveto the element’s actual viewport coordinates, not document coordinates after scrolling. - Dispatch the event after the element is present and visible.
- Wait for the site’s event handler or CSS transition before rendering.
- Some interfaces require a sequence such as mouse movement followed by a click; add only the events the page actually needs.
The pointer is offset
- Verify
page.viewportSize, zoom and any device-pixel scaling. - Use
getBoundingClientRect()for element-relative placement. - Account for scroll position when using
position: absoluteor when compositing onto a full-page image.
The page fails to load
Check the status passed to page.open, increase resourceTimeout for slow legacy sites, and log console or resource errors while diagnosing. PhantomJS development is suspended until further notice, so behavior can differ between old installations; record the exact executable and version when a capture matters.
Reliability and maintenance considerations
PhantomJS is a legacy headless browser. Its project homepage states that development is suspended, which means modern JavaScript, TLS, fonts and browser APIs may not behave like they do in current Chromium-based tools. For a fixed internal workflow, pin the PhantomJS version, keep a known viewport, and compare output images after dependency changes.
Keep the cursor node uniquely identified and remove it if the page remains open for additional captures:
Recommended Free Tools
page.evaluate(function () {
var cursor = document.getElementById('__phantom_cursor_overlay__');
if (cursor) {
cursor.parentNode.removeChild(cursor);
}
});
When a screenshot is an audit artifact, store the URL, viewport, cursor coordinates, output format and script revision alongside the image. That metadata makes an apparently misplaced pointer diagnosable instead of mysterious.
Best Value
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. It accepts a URL and returns PNG, JPEG, WebP or PDF without requiring you to maintain PhantomJS. 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, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and whether the request was billed.
Use the API documentation at https://screenshotneo.com/docs/ for the complete parameter list. A basic call is:
curl -G 'https://api.screenshotneo.com/v1/shot' -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Python:
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)
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(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', data);
For workflows that need more than a basic shot, ScreenshotNeo also supports full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper settings and page ranges, custom CSS and JavaScript, pre-capture clicks, waits for selectors or network idle, request and resource blocking, custom headers, cookies, user agents and Authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed public-image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API and an OpenAPI specification. Its parameter names are compatible with those used by other screenshot APIs, which can simplify migration. An MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Pricing is Free for 1,000 shots per month with no card, then Starter at $5 for 3,000, Growth at $15 for 15,000, Pro at $39 for 60,000, Scale at $99 for 250,000 and Business at $249 for 1,000,000; yearly billing gives two months free, and every feature is included on every plan. If you want clean captures without installing or maintaining a legacy browser, create a free ScreenshotNeo account (1,000 screenshots a month, no card).
Frequently asked questions
Can I use the overlay on a PDF render?
The overlay is page content, so it is available to the renderer in the same way as other DOM elements. Whether it appears on a particular PDF page depends on the PDF capture’s clipping and pagination; test the page range and position you plan to deliver.
Should I choose a custom cursor image or CSS?
Use CSS for a lightweight pointer whose size and color can change with coordinates or themes. Use a raster or SVG asset when visual fidelity and consistent branding matter more than keeping the page self-contained.
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.

