Element Screenshot API — Capture Any CSS Selector

Screenshot any HTML element by CSS selector. One API call replaces Puppeteer element.screenshot() with no browser to manage and no coordinate math. Free tier included.

Vitalii Holben Updated

A full-page screenshot of a typical e-commerce site weighs 8-12 MB and takes 4-5 seconds to render. If you only need the pricing table, that's a lot of wasted bandwidth and compute. The selector parameter tells the API to capture one HTML element and skip the rest. The response comes back faster and the file is a fraction of the size.

curl "https://api.screenshotrun.com/v1/screenshots/capture?\
url=https://example.com/pricing\
&selector=.pricing-table\
&format=png" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --output pricing-table.png

The API loads the page in a real browser, finds the first element matching your CSS selector, and clips the capture to that element's visible area. Everything outside is gone.

Why capture one element instead of the full page?

Full-page screenshots work when you need the whole picture. But most automation tasks target a specific piece of content. A chart from Grafana. A product card from an e-commerce catalog. A tweet you need for a report. Capturing the full page and cropping it afterward adds a processing step that breaks whenever the layout shifts.

Component testing is the most common use. Capture individual UI elements (navbars, forms, cards) for visual regression tests. You avoid false positives from unrelated layout changes because the screenshot covers only the component that changed.

Dashboard capture is the second big one. Pull a single Grafana or Metabase panel into Slack alerts or weekly reports without screenshotting the entire dashboard chrome.

Competitive monitoring: screenshot a competitor's pricing table daily, feed the image into OCR, track changes over time. For OG image generation, build a 1200×630 HTML template and capture just the card div as your Open Graph image. No canvas libraries needed.

How element capture works under the hood

When you pass selector=.pricing-table, the renderer does four things:

  1. Loads the full page at your specified viewport width (default 1280px, the most common desktop resolution across analytics data). CSS media queries, flexbox, and responsive breakpoints all apply normally.
  2. Scrolls the target element into view. Lazy-loaded images inside it trigger because the browser scrolls to the element's position.
  3. Calls the browser's native element.screenshot() method, which calculates the bounding box via getBoundingClientRect() (returns the element's exact position and size in pixels), handles subpixel rendering, and respects CSS transforms.
  4. Returns the cropped image. The response includes the element's width and height in the metadata, so you can verify the selector matched the right element without opening the file.

One detail that matters: the viewport affects how the element renders. A .pricing-table at 1280px wide might show three columns. At 480px? Single column, completely different layout. Set your viewport width to match what you actually need before capturing.

Element screenshot vs. full-page crop

Developers sometimes take a full-page screenshot and crop it with Sharp, Pillow, or ImageMagick. Think of it like cutting a photo out of a magazine with scissors: it works, but you need steady hands and the right coordinates. The selector approach reframes the shot at the source.

The crop approach has two practical problems. First, you need bounding box coordinates, and those change every time the page layout shifts (a new banner, an A/B test variant, an extra line of text above your target). Second, retina screenshots double both dimensions, so your coordinate math needs a multiplier you'll forget to update.

The third problem is subtler. Cropping can't handle elements that render differently based on scroll position. Intersection Observer animations (the browser API that fires when an element scrolls into view), lazy-loaded content, scroll-triggered charts. None of that fires when you screenshot the full page from the top.

With selector, the browser finds the element and captures it natively. No coordinate math and no post-processing pipeline to maintain.

What selectors work

Any valid CSS selector works. The API uses querySelector() internally, so it returns the first match.

Selector typeExampleWhen to use
ID#hero-bannerUnique element, most reliable
Class.product-cardFirst matching card (watch for duplicates)
Data attribute[data-testid="checkout-form"]React/Vue test IDs, stable across deploys
Nestedmain > section:nth-child(2)Structural selection when no ID or class exists
Attributetable[class*="pricing"]Partial class match for dynamically-named classes

A quick way to find the right selector: open Chrome DevTools, right-click the element, choose "Copy selector." Test it in the console with document.querySelector('your-selector') to confirm it returns one element.

When the selector doesn't match

If no element matches your selector, the API falls back to a regular viewport screenshot instead of returning an error. You'll get a valid image, but it will be the full viewport rather than the element you expected. Check the response dimensions. If the width and height match your viewport (1280×800 by default), the selector probably missed.

This catches people off guard the first time. I'd rather the API returned a 404, honestly, but the fallback exists because some automation pipelines prefer a viewport screenshot over no screenshot at all. If you want strict matching, compare the response dimensions against your expected element size in your code.

Combine selector with other parameters

Transparent background

curl "https://api.screenshotrun.com/v1/screenshots/capture?\
url=https://example.com\
&selector=.logo-container\
&omit_background=true\
&format=png" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --output logo-transparent.png

Grab a component with no background for compositing into mockups, pitch decks, or design tools. Only works with PNG, WebP, and AVIF (JPEG doesn't support alpha channels). If the element itself has a CSS background color, inject CSS to remove it: css=.logo-container { background: transparent !important }

Retina resolution

curl "https://api.screenshotrun.com/v1/screenshots/capture?\
url=https://example.com\
&selector=.hero-section\
&retina=true\
&format=png" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --output hero-2x.png

The output renders at 2x device pixel ratio, so the image is twice the width and height of the element's CSS dimensions. Worth knowing: retina screenshots are 4x the file size, not 2x, because both axes double.

Wait for dynamic content, then capture

curl "https://api.screenshotrun.com/v1/screenshots/capture?\
url=https://dashboard.example.com\
&selector=.chart-panel\
&wait_for_selector=.chart-panel canvas\
&delay=1000\
&format=png" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --output chart.png

For dashboards where charts render asynchronously: wait_for_selector pauses until the canvas element inside your target panel appears in the DOM. The extra delay gives the chart library a moment to finish drawing. Without this combo, you capture a loading spinner.

Click first, then capture

curl "https://api.screenshotrun.com/v1/screenshots/capture?\
url=https://example.com/faq\
&click_selector=.faq-item:first-child button\
&selector=.faq-item:first-child\
&format=png" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --output faq-expanded.png

Open an accordion, expand a dropdown, switch a tab. The click_selector fires before the element capture, so you screenshot the result of the interaction.

Full combination table

Pair withParameterWhat it does
Transparent backgroundomit_background=trueIsolate the element with no background
Retinaretina=true2x resolution for design review
Dark modedark_mode=trueCapture the element in dark theme
Wait for selectorwait_for_selector=...Wait for dynamic content inside the element
CSS injectioncss=...Style the element before capture (borders, padding, background)
Clickclick_selector=...Interact with the page before capturing the element
Output formatformat=webpSmaller files for thumbnails and previews

One parameter instead of a pipeline

Puppeteer and Playwright both have element.screenshot(), and if you're already running a headless browser in production, you can capture elements directly. But in practice it means writing and maintaining a whole pipeline: launch the browser, navigate to the page, wait for network idle, find the element with page.$(), handle the case where it doesn't exist, take the screenshot, close the browser. That's about 30 lines of code for a single capture, and none of it accounts for the edge cases you'll hit along the way.

Puppeteer has a documented bug where element.screenshot() captures the wrong bounding box in non-headless mode. Elements with overflow: scroll sometimes render as empty boxes. Fixed headers can duplicate in full-page stitched captures because they're rendered at every scroll position. And each browser tab chews through a few hundred megabytes of RAM, so at any real scale you're managing memory, patching Chromium versions, and configuring Docker's shared memory (/dev/shm defaults to 64 MB, which crashes Chrome on image-heavy pages).

With the API, all of that is one parameter. Add &selector=.pricing-table to your request URL, and you get back a clean image cropped to exactly the element you asked for. The browser lifecycle, the wait logic, the element lookup, the memory management, the edge cases with fixed positioning and overflow — that's our problem, not yours.

The selector parameter works with everything else the API offers: transparent backgrounds, dark mode, retina resolution, CSS injection, wait conditions, and click actions. You're not adding a new tool to your stack. You're adding one query parameter to a request you're already making.

FAQ#

The API falls back to a regular viewport screenshot (1280×800 by default) instead of returning an error. You'll get a valid image, but it will show the full viewport rather than the element you expected. Check the response dimensions to confirm whether the selector matched — if width and height equal your viewport size, the selector likely missed.

The selector parameter uses querySelector(), which returns the first match only. To capture multiple elements, send a separate API request for each selector. If you need several elements from the same page, the overhead is minimal since each request reuses the same page load logic on our side.

Standard CSS selectors can't reach into shadow roots. If your target element lives inside a closed shadow DOM (common in web components like Shoelace or Ionic), querySelector() won't find it. Workaround: use the js parameter to pierce the shadow root and move the element into the main DOM before capture.

The viewport determines how the element renders before capture. A pricing table at 1280px wide might display three columns, but the same table at 480px stacks into a single column. Set the width parameter to match the layout you need. The element screenshot will reflect whatever the CSS produces at that viewport width.

You can, but it's usually unnecessary. The selector parameter already scrolls to the target element and captures it at its full rendered size. Combining with full_page=true can cause issues with fixed-position elements (sticky headers, floating buttons) because they render at every scroll position during the full-page stitch. For element captures, skip full_page and let the selector handle it.

Chrome has a maximum texture size of about 16,384 pixels. Elements taller or wider than that get truncated. For very long tables or feeds, paginate the content or capture in sections. In practice, most UI components are well within this limit.

Written by

Vitalii Holben

Co-founder and CEO of ScreenshotRun. Full-stack developer building screenshot API tools that help developers capture, automate, and monitor web pages at scale. Based in Europe, shipping code since 201...

Screenshot API in a few clicks

Free plan includes 200 screenshots every month. No credit card required.

Sign up → copy your key → make your first call