You want a chart, a table, a card or an infobox, not the whole page. Here is code for each common tool that we ran against real pages (MDN and a long Wikipedia article), plus the two problems you will actually hit: elements taller than the viewport and captures that hang.
import { chromium } from "playwright";
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 800 }, deviceScaleFactor: 2 });
await page.goto("https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/table");
await page.locator("table").first().screenshot({ path: "table.png", animations: "disabled", timeout: 15000 });
// Same element with 16px of padding: clip a page screenshot to its box
const box = await page.locator("table").first().boundingBox();
const pad = 16;
await page.screenshot({
path: "table-padded.png",
clip: { x: box.x - pad, y: box.y - pad, width: box.width + 2 * pad, height: box.height + 2 * pad },
});
await browser.close();
Save it as shot.mjs (it uses top-level await). The locator waits for the element to be visible, scrolls it into view and crops to its box. Use .first() when the selector can match more than one element: Playwright's strict mode throws otherwise (Wikipedia's Kyoto article has two table.infobox). deviceScaleFactor: 2 doubles the pixels, so a 689px wide table comes out 1378px wide.
import puppeteer from "puppeteer";
const browser = await puppeteer.launch({ args: ["--no-sandbox"] });
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 2 });
await page.goto("https://en.wikipedia.org/wiki/Kyoto", { waitUntil: "networkidle2" });
const el = await page.waitForSelector("table.infobox", { visible: true });
await el.screenshot({ path: "infobox.png" });
await browser.close();
That infobox is about 2,500px tall, three times the viewport, and Puppeteer still captures all of it (648×5002 at 2x). waitForSelector returns the first match. --no-sandbox is needed for Chrome on Ubuntu 23.10+ and in most containers; drop it if your Chrome starts without it.
Selenium's element.screenshot("x.png") only captures the part of the element inside the viewport. On the Kyoto infobox it saved a 324×233 strip of a 2,500px element. In Chrome, ask DevTools directly instead:
import base64
from selenium import webdriver
from selenium.webdriver.common.by import By
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1280,800")
driver = webdriver.Chrome(options=options)
driver.get("https://en.wikipedia.org/wiki/Kyoto")
el = driver.find_element(By.CSS_SELECTOR, "table.infobox")
box = driver.execute_script(
"const r = arguments[0].getBoundingClientRect();"
"return {x: r.x + scrollX, y: r.y + scrollY, width: r.width, height: r.height};", el)
shot = driver.execute_cdp_cmd("Page.captureScreenshot", {
"format": "png", "captureBeyondViewport": True, "clip": {**box, "scale": 1},
})
with open("infobox.png", "wb") as f:
f.write(base64.b64decode(shot["data"]))
driver.quit()
The clip is in page coordinates (hence + scrollX/+ scrollY), captureBeyondViewport lets it extend past the window, and "scale": 2 gives retina output. Result: the full 323×2507 infobox. execute_cdp_cmd only exists on Chrome and Edge drivers. For a short element that fits on screen, plain el.screenshot() is fine.
On some heavy pages locator.screenshot() (and fullPage/clip screenshots) waits until it times out, even though the element is right there. We hit it on Wikipedia articles with every Chrome build we tried, even with JavaScript disabled, while Puppeteer and raw DevTools calls worked on the same page. Pass a timeout so it fails fast, and fall back to the same CDP call Selenium uses above:
import { chromium } from "playwright";
import { writeFileSync } from "node:fs";
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await page.goto("https://en.wikipedia.org/wiki/Kyoto");
// Ask Chrome directly through CDP, with the box in page coordinates
const box = await page.locator("table.infobox").first().evaluate(el => {
const r = el.getBoundingClientRect();
return { x: r.x + scrollX, y: r.y + scrollY, width: r.width, height: r.height };
});
const cdp = await page.context().newCDPSession(page);
const { data } = await cdp.send("Page.captureScreenshot", {
format: "png", captureBeyondViewport: true, clip: { ...box, scale: 2 },
});
writeFileSync("infobox.png", Buffer.from(data, "base64"));
await browser.close();
That produced the 648×5014 infobox in about 3 seconds. CDP sessions only work with Chromium, not Firefox or WebKit.
await page.addStyleTag({ content: "header { position: static !important }" }).waitForSelector on something inside the element), and in Playwright pass animations: "disabled".await el.evaluate(e => Promise.all([...e.querySelectorAll("img")].map(i => i.decode().catch(() => {})))).omitBackground: true (Playwright/Puppeteer, PNG only) to keep transparency outside the element's own background.data-* attributes or ARIA roles.If you only need the picture, Shotmint runs the browser for you: pass selector and you get that element back. It handles tall elements and the Wikipedia case above (it uses the CDP path internally).
curl -G "https://shotmint.creasework.com/v1/screenshot" \
-H "X-API-Key: $SHOTMINT_KEY" \
--data-urlencode "url=https://en.wikipedia.org/wiki/Kyoto" \
--data-urlencode "selector=table.infobox" \
-d device_scale_factor=2 \
-o infobox.png
import os, requests
r = requests.get("https://shotmint.creasework.com/v1/screenshot", params={
"url": "https://en.wikipedia.org/wiki/Kyoto",
"selector": "table.infobox", # first match, like querySelector
"device_scale_factor": 2,
"hide_selectors": ".vector-sticky-header",
}, headers={"X-API-Key": os.environ["SHOTMINT_KEY"]}, timeout=60)
r.raise_for_status()
open("infobox.png", "wb").write(r.content)
| In your own browser code | Shotmint |
|---|---|
locator(sel).screenshot() / el.screenshot() | selector=sel (first match, waits up to 5s for it to be visible) |
deviceScaleFactor: 2 | device_scale_factor=2 |
addStyleTag to hide headers or banners | hide_selectors=.header,.banner or block_ads=true |
omitBackground: true | omit_background=true |
type: "jpeg", quality | format=jpeg or webp, quality |
Padding around the element isn't a parameter. Wrap the element in your own page and use the html body, or pad the image afterwards. If a selector doesn't match within 5 seconds you get a 400 with a clear message, and failed renders are refunded. One render = one credit: 100 free with a free key, then €5 for 2,000 with no subscription. To cut an element out of an image you already have, use the crop image tool. For whole pages, see Playwright screenshots and Puppeteer screenshots.
No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire).
Get a free API key Full API reference