How to Use Print Stylesheets with PhantomJS for Node.js
Generate PDFs with PhantomJS and Node.js using print CSS, paper sizes, readiness checks, pagination rules, and practical fixes for missing styles.

Use a separate print stylesheet, wait until the page is actually ready, set page.paperSize, and call page.render() with a .pdf filename. PhantomJS selects print media rules while rendering PDF, but it uses an old WebKit engine, so pagination and newer CSS features must be checked against the PhantomJS binary you deploy.
This guide shows a complete Node.js workflow, including the PhantomJS script, process management, asynchronous pages, page geometry, headers and footers, page breaks, debugging, and production alternatives.
1. How print stylesheets work in PhantomJS
Print CSS is ordinary CSS selected for a print media context. Put rules in a linked stylesheet with media='print', or place them inside an @media print block in a stylesheet that is loaded for every media type.
<link rel='stylesheet' href='/css/site.css'>
<link rel='stylesheet' media='print' href='/css/print.css'>
/* print.css */
@page {
size: A4 portrait;
margin: 16mm 14mm 18mm;
}
@media print {
.screen-only,
nav,
.cookie-banner,
.chat-widget {
display: none !important;
}
body {
color: #000;
background: #fff;
font: 10.5pt/1.45 Georgia, serif;
}
a {
color: inherit;
text-decoration: none;
}
.invoice,
.report-section {
break-inside: avoid;
}
h2 {
break-after: avoid;
}
.page-break {
break-before: page;
}
}
The linked stylesheet must finish loading before rendering. If the page builds its report with JavaScript, images, web fonts, or API data, a successful page.open callback alone is not enough. Add a readiness flag in the page and wait for it from the PhantomJS process.
2. Install the Node.js and PhantomJS pieces
PhantomJS is a separate executable. Node.js starts it, passes a script and URL, and collects the generated PDF. A maintained installation of the PhantomJS binary is required on every machine that renders documents.

mkdir phantom-print-demo
cd phantom-print-demo
npm init -y
npm install phantomjs-prebuilt
The package above supplies a binary path for local development. In production, pin the binary version and verify that the executable is available in your container or host. The Node wrapper can be replaced with another PhantomJS launcher if your deployment already manages the binary.
3. Build a page with a readiness signal
Add a signal that becomes true only after your application has inserted the content needed in the PDF. A static document can set it immediately after the DOM and assets are available.
<script>
window.pdfReady = false;
// Replace this with your real data-loading code.
fetch('/api/report/42')
.then(function (response) { return response.json(); })
.then(function (report) {
document.querySelector('#report-title').textContent = report.title;
document.querySelector('#report-total').textContent = report.total;
window.pdfReady = true;
})
.catch(function () {
window.pdfReady = 'error';
});
</script>
If you cannot change the page, wait for a stable selector such as #report-total, or use a conservative delay after page.open. A selector or explicit flag is more reliable than a fixed delay because network and rendering time vary.
4. Complete PhantomJS PDF script
Create render.js. This script runs inside PhantomJS, not inside Node.js. It opens a URL, sets paper geometry before rendering, waits for readiness, and exits with a useful status code.
var system = require('system');
var webpage = require('webpage');
if (system.args.length < 3) {
console.error('Usage: phantomjs render.js URL OUTPUT.pdf');
phantom.exit(2);
}
var url = system.args[1];
var output = system.args[2];
var page = webpage.create();
page.settings.userAgent = 'PhantomJS PDF renderer';
page.settings.resourceTimeout = 30000;
page.viewportSize = { width: 1280, height: 900 };
page.onResourceError = function (error) {
console.error('Resource error: ' + error.url + ' (' + error.errorString + ')');
};
page.onConsoleMessage = function (message) {
console.log('[page] ' + message);
};
page.paperSize = {
format: 'A4',
orientation: 'portrait',
margin: {
top: '16mm',
right: '14mm',
bottom: '18mm',
left: '14mm'
},
header: {
height: '8mm',
contents: phantom.callback(function (pageNum, numPages) {
return '<div style="font-size:8pt;text-align:right;color:#666">' +
pageNum + ' / ' + numPages + '</div>';
})
},
footer: {
height: '8mm',
contents: phantom.callback(function (pageNum, numPages) {
return '<div style="font-size:8pt;text-align:center;color:#666">' +
'Generated report · ' + pageNum + ' / ' + numPages + '</div>';
})
}
};
function finish(code) {
window.setTimeout(function () { phantom.exit(code); }, 0);
}
function waitForReady(startedAt) {
var state = page.evaluate(function () {
return {
ready: window.pdfReady === true,
failed: window.pdfReady === 'error',
selector: !!document.querySelector('#report-title')
};
});
if (state.failed) {
console.error('The page reported a data-loading error');
finish(1);
return;
}
if (state.ready || state.selector) {
page.render(output);
console.log('Wrote ' + output);
finish(0);
return;
}
if (Date.now() - startedAt > 30000) {
console.error('Timed out waiting for PDF readiness');
finish(1);
return;
}
window.setTimeout(function () { waitForReady(startedAt); }, 250);
}
page.open(url, function (status) {
if (status !== 'success') {
console.error('Unable to open ' + url + ': ' + status);
finish(1);
return;
}
waitForReady(Date.now());
});
paperSize defines the physical page used for PDF output. You can use a named format such as A4 or Letter, set orientation to portrait or landscape, and provide margins as mm, cm, in, or px. Explicit dimensions are useful for receipts and labels:
page.paperSize = {
width: '80mm',
height: '180mm',
margin: '3mm'
};
Headers and footers are optional. Keep their heights large enough for the returned markup. If you do not need them, omit those properties. The render call chooses PDF output from the .pdf extension.
5. Start PhantomJS from Node.js
This Node program validates arguments, launches PhantomJS, streams diagnostics, and rejects non-zero exits. It is suitable for a command-line job or a server-side service wrapper.
const phantomPath = require('phantomjs-prebuilt').path;
const { spawn } = require('node:child_process');
const path = require('node:path');
function renderPdf(url, outputPath) {
return new Promise((resolve, reject) => {
const child = spawn(phantomPath, [
path.join(__dirname, 'render.js'),
url,
outputPath
], { stdio: ['ignore', 'pipe', 'pipe'] });
let stderr = '';
child.stdout.on('data', data => process.stdout.write(data));
child.stderr.on('data', data => {
stderr += data.toString();
process.stderr.write(data);
});
child.on('error', reject);
child.on('close', code => {
if (code === 0) resolve(outputPath);
else reject(new Error(`PhantomJS exited ${code}: ${stderr}`));
});
});
}
const url = process.argv[2] || 'http://localhost:3000/report';
const output = process.argv[3] || '/tmp/report.pdf';
renderPdf(url, output)
.then(file => console.log(`Ready: ${file}`))
.catch(error => {
console.error(error.message);
process.exitCode = 1;
});
node run-render.js http://localhost:3000/report /tmp/report.pdf
6. Print CSS options that matter for PDFs
| Goal | CSS or PhantomJS setting | Practical note |
|---|---|---|
| Hide controls | display:none in @media print |
Use !important when application styles win specificity. |
| Set page dimensions | @page plus page.paperSize |
Keep both aligned; the PhantomJS setting controls the PDF canvas. |
| Prevent split cards | break-inside: avoid |
Older WebKit may only partially support modern break properties. |
| Force a new page | break-before: page or legacy page-break-before: always |
Use legacy properties when targeting old PhantomJS builds. |
| Repeat table headings | thead { display: table-header-group; } |
Check long tables because old engines differ on row splitting. |
| Show URLs | a[href]::after { content: ' (' attr(href) ')'; } |
Limit this to print rules so screen links remain clean. |
| Control colors | -webkit-print-color-adjust: exact |
Support varies; design a readable grayscale fallback. |
Do not rely on flexbox, grid, custom properties, modern selectors, or web-font behavior without checking your exact PhantomJS version. When a layout is unstable, replace it with simple block, table, or float layout for the print stylesheet.
7. Handling images, fonts, and JavaScript content
- Use absolute or fully qualified asset URLs when the PDF job runs outside the web server’s working directory.
- Confirm the PhantomJS process can resolve DNS, reach private services, and authenticate to protected assets.
- Wait for an application readiness flag after data, images, and fonts are loaded.
- Give each render a deadline and terminate the child process if it exceeds that deadline.
A page can report success while one image or stylesheet has failed. Log page.onResourceError and inspect the generated PDF, not only the navigation status. For deterministic documents, inline critical print CSS and use local or stable asset URLs.
8. Common errors and fixes
Print rules are ignored
Check that the stylesheet is loaded, that the rules are inside @media print or a media='print' link, and that the selector is not overridden by a more specific rule. Confirm you are rendering a PDF, not taking a screen image. Add a temporary unmistakable print rule, such as a black background on a test block, to prove the print context is active.
The PDF is blank or missing data
Rendering probably happened before JavaScript finished. Add window.pdfReady = true after the final update and wait for it. Also inspect console and resource errors, API authentication, and cross-origin restrictions.
Styles or images work in Chrome but not PhantomJS
PhantomJS uses an older WebKit engine. Replace unsupported CSS with older equivalents, avoid relying on modern layout features, and use absolute asset URLs. Test with the same binary and operating system used in deployment.
Pages split in the wrong places
Use break-inside: avoid and legacy page-break-inside: avoid on compact blocks. Add explicit page breaks before major sections. Very tall elements cannot fit on one page, so allow them to split or redesign them.
Headers overlap the content
Increase the top or bottom margin and the corresponding header or footer height. Keep header markup small and avoid external assets inside it.
The Node process hangs
Ensure every success and failure path calls phantom.exit(). Add a parent-process timeout, kill the child if necessary, and remove temporary files after failed jobs.
Protected pages return a login screen
Pass cookies or authentication headers through the page before opening the URL, or render an internal endpoint that is authenticated for the job. Never put long-lived credentials in a public URL.
9. Performance, reliability, and cost
Starting a PhantomJS process for every request is simple but expensive in CPU and startup time. A queue with bounded concurrency prevents a traffic spike from exhausting memory. Reuse a worker only if you can reset cookies, local storage, page state, and timers between jobs; isolated processes are easier to reason about.
Cache PDFs when the source data and print options have not changed. Include the URL, relevant parameters, stylesheet version, and data revision in the cache key. Avoid waiting for an arbitrary long delay: a readiness selector reduces both latency and incomplete documents.
Reliability depends on the page as much as the renderer. Record the URL, PhantomJS version, render duration, exit code, output size, and resource failures. Keep the original HTML or a request identifier for failed jobs so you can reproduce a layout issue. Validate output size and, where possible, open the PDF in a parser before marking a job successful.
Local PhantomJS has no per-render API fee, but you pay for hosts, process supervision, maintenance, and engineering time. A hosted renderer trades process operations for a service charge and may offer print-media emulation, page ranges, templates, or managed retries. Compare print-CSS fidelity, paper and margin controls, asynchronous-content handling, header/footer support, page ranges, and operational burden before switching.
10. Or skip the browser setup
ScreenshotNeo provides a hosted screenshot and PDF API. It accepts one GET request, applies print-oriented rendering controls, and returns a PDF or image. See the ScreenshotNeo API documentation for all parameters.

curl -G 'https://api.screenshotneo.com/v1/shot' \
-d access_key=YOUR_API_KEY \
--data-urlencode 'url=https://stripe.com' \
-d format=pdf \
-o report.pdf
import requests
r = requests.get(
'https://api.screenshotneo.com/v1/shot',
params={
'access_key': 'YOUR_API_KEY',
'url': 'https://stripe.com',
'format': 'pdf'
},
timeout=90
)
r.raise_for_status()
open('report.pdf', 'wb').write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com',
format: 'pdf'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
require('node:fs').writeFileSync('report.pdf', Buffer.from(await res.arrayBuffer()));
Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether it was billed. ScreenshotNeo also provides an MCP server so Claude, Cursor, and other MCP clients can use take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots each month with no card, and paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
11. Production checklist
- Print stylesheet is loaded with the intended media attribute.
- Paper format, orientation, and margins are set before
render. - Dynamic content exposes a readiness flag or stable selector.
- Images, fonts, and API calls are reachable from the renderer.
- Legacy page-break rules cover the PhantomJS engine in use.
- Every child process has a timeout and cleanup path.
- Logs include resource errors, exit codes, duration, and output size.
- Representative long tables, images, and multi-page sections are reviewed.
12. FAQ
Does PhantomJS automatically use @media print?
PDF rendering uses print styling, but your stylesheet still must contain print rules or a print-only linked file. Verify with a temporary print-specific rule.
Can I generate a PDF from an HTML string?
Yes. Use page.setContent in the PhantomJS script, then wait for local assets and generated content before calling render.
Can I set a custom receipt size?
Yes. Set page.paperSize.width and height with units such as mm or px, plus a small margin.
Why does a fixed delay sometimes fail?
Network speed, API latency, image decoding, and JavaScript execution vary. A readiness flag or selector expresses the actual condition you need.
Should I use PhantomJS for new projects?
Use it when you must support an existing PhantomJS pipeline and have validated its older WebKit behavior. For new systems, compare a maintained browser or hosted PDF service against your CSS and operational requirements.


