How to Include the URL in a Playwright Screenshot
Learn how to show a page URL inside a Playwright screenshot, preserve it as metadata, or add it to a PDF header.
Playwright screenshots contain the rendered page, not the browser address bar. Playwright does not document a screenshot option that adds the current URL as browser chrome or as an automatic image header. To make the URL visible, read page.url(), render that value in a page overlay, and then call page.screenshot(). If you only need the URL for filing or audit purposes, save it beside the image instead of changing the pixels.
This guide shows a complete implementation in JavaScript, including full-page and element captures, idempotent overlays, long URLs, cleanup, timing, PDF headers, troubleshooting, and an API alternative.
What Playwright captures
The official screenshots guide documents viewport screenshots, full-page screenshots, element screenshots, and screenshots returned as a buffer. A full-page screenshot is the entire scrollable page rendered as if the page were very tall. It changes the capture area; it does not add the browser toolbar or address bar.
The current URL is available through page.url() in the Node.js API. That value can be rendered as normal HTML before capture.
Add a visible URL label before the screenshot
The following script is runnable with Playwright. It opens a page, creates a fixed URL label, captures a PNG, removes the label, and closes the browser.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
});
await page.goto('https://example.com', { waitUntil: 'networkidle' });
const url = page.url();
await page.evaluate((currentUrl) => {
const existing = document.querySelector('[data-playwright-url-label]');
existing?.remove();
const label = document.createElement('div');
label.dataset.playwrightUrlLabel = 'true';
label.textContent = currentUrl;
Object.assign(label.style, {
position: 'fixed',
top: '0',
left: '0',
right: '0',
zIndex: '2147483647',
boxSizing: 'border-box',
padding: '8px 12px',
background: '#fff',
color: '#111',
font: '14px sans-serif',
lineHeight: '1.4',
overflowWrap: 'anywhere',
boxShadow: '0 1px 4px #0004',
});
document.body.appendChild(label);
}, url);
await page.screenshot({
path: 'screenshot-with-url.png',
fullPage: true,
});
await page.evaluate(() => {
document.querySelector('[data-playwright-url-label]')?.remove();
});
await browser.close();
Install and run it with:
npm install playwright
node screenshot-with-url.mjs
The data-playwright-url-label marker makes the operation idempotent. Running the helper twice replaces the old label instead of stacking two labels.
Overlay versus pushing the page down
A fixed label overlays the first part of the page. This keeps the page layout unchanged, but it can cover a heading or navigation. To reserve space, use a normal block at the start of body instead:
await page.evaluate((currentUrl) => {
document.querySelector('[data-playwright-url-label]')?.remove();
const label = document.createElement('div');
label.dataset.playwrightUrlLabel = 'true';
label.textContent = currentUrl;
Object.assign(label.style, {
boxSizing: 'border-box',
width: '100%',
padding: '8px 12px',
background: '#fff',
color: '#111',
font: '14px sans-serif',
overflowWrap: 'anywhere',
borderBottom: '1px solid #ddd',
});
document.body.prepend(label);
}, page.url());
await page.screenshot({ path: 'screenshot-with-url-and-offset.png' });
Use the overlay when preserving the page’s geometry matters. Use the block when readers must see every top-of-page element without obstruction.
Reusable helper for viewport, full-page, and element screenshots
Put the label logic in a helper so every capture uses the same styling and cleanup policy.
async function addUrlLabel(page, {
position = 'fixed',
background = '#fff',
color = '#111',
fontSize = '14px',
} = {}) {
const currentUrl = page.url();
await page.evaluate(({ currentUrl, position, background, color, fontSize }) => {
document.querySelector('[data-playwright-url-label]')?.remove();
const label = document.createElement('div');
label.dataset.playwrightUrlLabel = 'true';
label.textContent = currentUrl;
Object.assign(label.style, {
position,
top: '0',
left: '0',
right: '0',
zIndex: '2147483647',
boxSizing: 'border-box',
padding: '8px 12px',
background,
color,
font: `${fontSize} sans-serif`,
lineHeight: '1.4',
overflowWrap: 'anywhere',
boxShadow: '0 1px 4px #0004',
});
document.body.appendChild(label);
}, { currentUrl, position, background, color, fontSize });
}
async function removeUrlLabel(page) {
await page.evaluate(() => {
document.querySelector('[data-playwright-url-label]')?.remove();
});
}
await addUrlLabel(page, { background: '#111', color: '#fff' });
await page.screenshot({ path: 'viewport.png' });
await page.screenshot({ path: 'full-page.png', fullPage: true });
const card = page.locator('[data-testid="pricing-card"]').first();
await card.screenshot({ path: 'element.png' });
await removeUrlLabel(page);
Choosing the screenshot scope
| Capture | Use it when | URL-label detail |
|---|---|---|
| Viewport | You need exactly the visible viewport. | A fixed label stays in the captured viewport. |
fullPage: true |
You need the complete scrollable document. | The label appears at the top of the tall image; an overlay can cover top content. |
| Element screenshot | You need one component or region. | A label attached to body may be outside the element and therefore absent. Put the URL inside the element or capture the page instead. |
Keep the URL out of the pixels
If the image itself should remain untouched, store the URL in a filename, JSON record, database row, or object metadata.
import { writeFile } from 'node:fs/promises';
const currentUrl = page.url();
const image = await page.screenshot({ type: 'png' });
await writeFile('page.png', image);
await writeFile('page.json', JSON.stringify({
url: currentUrl,
capturedAt: new Date().toISOString(),
file: 'page.png',
}, null, 2));
This is usually preferable for machine processing, pixel comparisons, and designs where an added banner would change the visual result.
Use a URL header in a PDF
Playwright’s Page API documents PDF header and footer templates. The url template class can print the document location. This applies to PDF output, not to page.screenshot().
await page.pdf({
path: 'page-with-url.pdf',
format: 'A4',
displayHeaderFooter: true,
headerTemplate: '',
footerTemplate: ' / ',
margin: { top: '18mm', bottom: '18mm' },
});
PDF template scripts are not evaluated, and page styles are not visible inside the templates. If the deliverable must be a PNG, JPEG, or WebP, use an HTML label instead.
cURL, Python, and Node.js alternatives
For a local browser workflow, Playwright gives you control over the DOM and capture timing. If you only need a clean image from a URL, a screenshot API removes the browser setup.
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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));
See the ScreenshotNeo API documentation for request options. ScreenshotNeo can capture PNG, JPEG, WebP, or PDF and supports custom CSS and JavaScript if you need to add a URL label remotely.
Or skip the browser setup
ScreenshotNeo accepts one GET request and returns the capture. It can remove cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Create a free ScreenshotNeo account to get 1,000 screenshots each month without a card.
Options and edge cases
Redirects
Call page.url() after navigation has settled so the label contains the final URL rather than the initial redirect target. If the page performs client-side navigation later, read the URL immediately before capture.
Long URLs and query strings
Use overflow-wrap: anywhere or word-break: break-word. Otherwise a long query string can extend beyond the viewport. For sensitive query parameters, redact the value before rendering while keeping the original URL in protected metadata.
Hash fragments
page.url() can include a fragment such as #pricing. Keep it when the fragment identifies the exact state being captured.
Authentication
Do not put passwords, tokens, or session cookies into a visible label. Capture the authenticated page, but render a redacted URL or store the full URL in access-controlled metadata.
Timing
Add the label after the page’s own content is ready. If you add it before late layout changes, the final image can still shift underneath it. Wait for a meaningful selector, an application-specific ready signal, or an appropriate network state before capturing.
Content security policy
page.evaluate() runs in the page context and is often more practical than loading a separate script or stylesheet. A restrictive application can still affect what the page permits, so keep the injected code self-contained and verify it against the target application’s constraints.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| The browser address bar is missing. | Playwright captures page content, not browser-window chrome. | Inject a visible label or use a browser-window capture tool outside Playwright. |
| The URL label is not visible. | The label was added outside the element being captured, or the screenshot ran before the script completed. | Await page.evaluate(); for element shots, add the label inside the target element or capture the page. |
| The label covers the heading. | A fixed overlay sits above the page. | Use a normal block that pushes content down, or change padding and opacity. |
| Two labels appear. | The helper was called more than once without cleanup. | Remove an existing [data-playwright-url-label] before appending. |
| The URL is truncated. | Overflow is clipped or the label has a fixed width. | Use overflowWrap: 'anywhere' and allow the label to span the viewport. |
| The screenshot shows the wrong URL. | Navigation or a client-side route change happened after the URL was read. | Read page.url() immediately before adding the label and capture. |
| PDF header is blank. | PDF templates have their own limitations and do not run arbitrary scripts. | Use the documented template classes, or render the URL in the page body before generating the PDF. |
| Capture fails on a private page. | The browser lacks the required authenticated context. | Create the context with the correct storage state, headers, or cookies, and never expose secrets in the label. |
Performance, reliability, and cost
- Performance: Injecting one small DOM node has little work compared with navigation, fonts, images, and full-page layout. Full-page screenshots still require the browser to render the entire scrollable document.
- Reliability: Make the helper idempotent, await it, capture only after the page is ready, and remove the label in a
finallyblock when the same page will be reused. - Repeatability: Fix the viewport, color scheme, device scale factor, locale, and authentication state when screenshots are used for visual comparisons.
- Cost: A self-hosted Playwright process costs whatever your browser infrastructure costs. With ScreenshotNeo, clean shots are billed; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Caching uses a TTL you choose.
try {
await addUrlLabel(page);
await page.screenshot({ path: 'capture.png', fullPage: true });
} finally {
await removeUrlLabel(page);
}
FAQ
Can Playwright include the browser’s address bar?
No. The documented screenshot API captures rendered page content. Use a page label or a separate browser-window capture.
Can I add the URL without changing the screenshot?
Yes. Save page.url() in a sidecar JSON file, database record, filename, or object metadata.
Does fullPage add the URL automatically?
No. It only expands the capture to the full scrollable page.
Should I use a PDF instead?
Use PDF when a printable document with a repeating header or footer is acceptable. Use an image overlay when the output must remain a PNG, JPEG, or WebP.
Can an element screenshot contain a page-level URL label?
Usually not if the label is attached to body. Capture the page, or place a label inside the element you are screenshotting.


