How to Convert an HTML Table to a PNG Image with Playwright
Capture an HTML table as a PNG with Playwright. Learn how to load markup, choose the capture scope and output, handle dynamic content, and fix common errors.
Use Playwright’s locator screenshot API to save just the table as a PNG:
await page.locator('table').screenshot({ path: 'table.png' });
For an HTML string, load it into a page first with page.setContent(html), then capture the table. PNG is the default screenshot format, and the locator screenshot can also return image bytes for in-memory use. See the official Playwright screenshots guide and Page API.
1. Capture a table from an HTML string
This complete Node.js example creates a page, supplies styled table markup, saves the table to table.png, and closes the browser even if capture fails. Install Playwright and its browser binaries using the instructions for your environment before running it.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
try {
const page = await browser.newPage();
await page.setContent(`
<!doctype html>
<html>
<head>
<style>
body { margin: 24px; }
table {
border-collapse: collapse;
font: 16px Arial, sans-serif;
color: #222;
}
th, td { border: 1px solid #888; padding: 8px 12px; }
th { background: #eee; text-align: left; }
</style>
</head>
<body>
<table>
<thead>
<tr><th>Item</th><th>Count</th></tr>
</thead>
<tbody>
<tr><td>Apples</td><td>12</td></tr>
<tr><td>Oranges</td><td>8</td></tr>
</tbody>
</table>
</body>
</html>
`);
await page.locator('table').screenshot({ path: 'table.png' });
} finally {
await browser.close();
}
})();
Run it with node capture-table.js. The markup and styling are examples; adapt the table and CSS to your output. The API behavior described here follows Playwright’s documentation; check the docs for the binding and version installed in your project.
2. Capture a table already on a page
If navigation or setup has already loaded the page, skip setContent. Select the intended table as specifically as possible so the locator resolves to the right element:
await page.goto('https://example.com/reports');
await page.locator('#results table').screenshot({ path: 'results-table.png' });
Replace the example URL and selector with your page and a selector that identifies one table. A page with several tables may make locator('table') ambiguous; use a container, ID, or other stable selector from your markup.
3. Wait for the final table content
Capture only after the rows and layout you need are ready. For a table populated asynchronously, wait for a meaningful application-specific condition before taking the screenshot. For example, if the page adds a row with a known value:
await page.goto('https://example.com/reports');
await page.locator('#results tbody tr').first().waitFor({ state: 'visible' });
await page.locator('#results table').screenshot({ path: 'results-table.png' });
Choose a condition that represents the data being ready for your page; waiting for the first row is not sufficient if the application fills in more rows later. Fonts, styles, viewport, and data affect the rendered pixels, so keep those inputs consistent when you need repeatable output.
4. Choose the capture scope and output
Only the table or the whole page
A locator screenshot captures the selected table as an element image:
await page.locator('table').screenshot({ path: 'table.png' });
To include surrounding page content and the full scrollable page, use a page screenshot instead:
await page.screenshot({ path: 'page.png', fullPage: true });
These calls produce different artifacts: the first targets a table; the second captures the page. For a scrollable element, an element screenshot shows only its currently scrolled content. If you need the entire page, choose the page-level capture and verify that this broader scope is appropriate.
Save to a file or keep the bytes
Passing path writes the image to a file. Without a path, the screenshot call returns image bytes that you can pass to another part of your program or save yourself:
const imageBytes = await page.locator('table').screenshot();
// imageBytes is a Buffer in Node.js and can be written or passed to another API.
Format and scale
- PNG: the default format. Use it for the title’s requested output.
- JPEG or WebP: supported screenshot formats when a different image format fits your use case. JPEG quality settings do not affect PNG output.
- CSS scale:
scale: 'css'produces one image pixel per CSS pixel. - Device scale:
scale: 'device'uses device pixels and can make the image larger on high-DPI devices.
For example, locator screenshot options can set scale and animation behavior:
await page.locator('table').screenshot({
path: 'table.png',
scale: 'css',
animations: 'disabled'
});
Disabling animations can help avoid capturing a table mid-transition. Confirm the option names against the screenshot API documentation for your installed Playwright version.
5. Use cURL, Python, or ScreenshotNeo when the table is on a public page
Playwright is a good fit when the table comes from HTML you generate, needs browser-side setup, or must be captured as a specific element. If the table is already visible on a publicly reachable page and you want an image of the page, a screenshot API can avoid managing a local browser. These API examples capture the target page; they do not select just one table element by CSS selector.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com/reports \
-o report.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/reports"},
timeout=90,
)
r.raise_for_status()
with open("report.webp", "wb") as f:
f.write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/reports'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await require('node:fs/promises').writeFile('report.webp', bytes);
See the ScreenshotNeo API documentation for request options and response details. ScreenshotNeo is a website screenshot API and MCP server by Yorker Media. Its capture options include full-page capture and element selection by CSS selector, so consult the docs if you need to target the table rather than capture the page.
Or skip the browser setup
Make one GET request to capture the public page as an image:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/reports -o shot.webp
ScreenshotNeo accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers say which page verdict occurred and whether the request was billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
6. Troubleshooting
| Problem | Likely cause | What to do |
|---|---|---|
| Locator screenshot times out or cannot find the table | The selector matches nothing yet, the wrong page is loaded, or the table is added later. | Check the page and selector, then wait for an application-specific table or data-ready condition. |
| Strict mode or multiple-match error | The selector resolves to multiple tables where the operation expects one target. | Scope it to the intended container or use a unique ID or test selector. |
| The image is empty or has no rows | The capture ran before asynchronous data populated the table, or the selected table is not the rendered one. | Wait for the expected content and confirm the locator points to the visible table. |
| Some content is missing from a long table | The table is inside a scrollable container, and an element screenshot captures only its currently scrolled content. | Use page-level full-page capture if you need the full page, or adjust the page/container setup to expose the content you need. |
| Text, spacing, or colors differ between runs | Rendered pixels depend on styles, fonts, viewport, and data; animations may also change the captured state. | Keep the rendering inputs stable and consider disabling animations for the screenshot. |
| PNG is larger than expected | PNG is lossless and can use more bytes than JPEG or WebP for some content. | Keep PNG when that format is required; otherwise choose a supported alternative format if file size matters more than PNG output. |
7. Performance, reliability, and cost
A locator screenshot captures one element, while a full-page screenshot captures more content. Choose the smallest scope that meets the output requirement. Return a buffer instead of writing a file when the next step can consume bytes directly. Use a readiness condition tied to the data you need so the capture does not run too early; avoid relying on arbitrary delays when the application exposes a better signal.
For reliable repeat captures, use a stable selector and hold the content, styles, fonts, viewport, scale, and animation state steady. Close the browser in a finally block so failures do not leave the process holding browser resources. Playwright itself runs in your environment, so account for the browser installation and runtime your workflow requires. No benchmark or fixed cost is implied here; actual runtime and infrastructure cost depend on the page and where you run the browser.
ScreenshotNeo’s stated plans are Free: 1,000 shots per month; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; and Business: $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Compare the API’s page-level and selector options in its docs with your need for a local Playwright browser and direct control over generated HTML.
8. FAQ
Does locator screenshot save PNG by default?
Yes. PNG is the documented default screenshot format.
Can I capture a table from an HTML string without a URL?
Yes. Set the page content with page.setContent(html), then capture a locator for the table.
Can I pass the screenshot to another function without creating a file?
Yes. Omit path and retain the returned image buffer.
Should I use a locator screenshot or fullPage?
Use a locator screenshot for the standalone table. Use a full-page page screenshot when the artifact should include the full page and its surrounding content.


