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:
- 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.
- Scrolls the target element into view. Lazy-loaded images inside it trigger because the browser scrolls to the element's position.
- Calls the browser's native
element.screenshot()method, which calculates the bounding box viagetBoundingClientRect()(returns the element's exact position and size in pixels), handles subpixel rendering, and respects CSS transforms. - 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 type | Example | When to use |
|---|---|---|
| ID | #hero-banner | Unique element, most reliable |
| Class | .product-card | First matching card (watch for duplicates) |
| Data attribute | [data-testid="checkout-form"] | React/Vue test IDs, stable across deploys |
| Nested | main > section:nth-child(2) | Structural selection when no ID or class exists |
| Attribute | table[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 with | Parameter | What it does |
|---|---|---|
| Transparent background | omit_background=true | Isolate the element with no background |
| Retina | retina=true | 2x resolution for design review |
| Dark mode | dark_mode=true | Capture the element in dark theme |
| Wait for selector | wait_for_selector=... | Wait for dynamic content inside the element |
| CSS injection | css=... | Style the element before capture (borders, padding, background) |
| Click | click_selector=... | Interact with the page before capturing the element |
| Output format | format=webp | Smaller 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.