Element screenshots

An element screenshot returns an image of one DOM node: a pricing table, a chart, a tweet embed, a product card. The output is cropped to that element's box, so you do not need to post-process a full-page image. This guide is part of the capture guides.

The minimal request

The examples read your key from RS_API_KEY. If you do not have a key yet, create a free account first.

With GET, pass selector. The API switches to element mode for you:

curl -G "https://api.renderscreenshot.com/v1/screenshot" \
  -H "Authorization: Bearer $RS_API_KEY" \
  --data-urlencode "url=https://example.com" \
  --data-urlencode "selector=body > div" \
  -o element.png

With POST, set the mode and the selector in capture:

curl -X POST https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "capture": {
      "mode": "element",
      "selector": "body > div",
      "selector_required": true
    }
  }' \
  -o element.png

On example.com, body > div is the centered text block, so the image is that block and not the 1280 x 800 viewport.

Parameters that matter

Parameter Default Notes
capture.mode viewport Set to element. The GET selector parameter sets it for you.
capture.selector none Any CSS selector that document.querySelector accepts. The first match is captured.
capture.selector_required false Set to true so a missing element fails the request instead of returning another image.
capture.beyond_viewport true Includes the parts of the element outside the visible window.
page.shadow_dom false Lets selectors reach elements inside open shadow roots.
viewport.width / viewport.scale 1280 / 1 The element's width depends on the viewport width; scale multiplies the output pixels.

Set selector_required: true in production. Without it, a redesign that renames a class can quietly change what you capture. With it, you get a target_error (HTTP 422) whose message names the selector, and your error handling can alert you.

Choosing a selector that survives redesigns

Element captures break when the selector breaks. In order of preference:

  1. A data attribute you add for this purpose: [data-screenshot="pricing"]. Nobody renames it by accident.
  2. An id: #pricing-table.
  3. A semantic structure: main article > figure:first-of-type.
  4. Utility classes such as .mt-8.grid.gap-4 are the most fragile. Build tools and CSS frameworks change them often.

Test the selector on the live page before you put it in code. Open DevTools on the target page and run:

// Paste into the DevTools console of the target page
const matches = document.querySelectorAll('[data-screenshot="pricing"]');
console.log(matches.length, matches[0]?.getBoundingClientRect());

matches.length should be 1. If it is 0, the element is created later by JavaScript or sits inside a shadow root or an iframe (see the table below). If it is more than 1, the API captures the first match only.

Element mode when the element loads late

The selector is evaluated after the wait conditions finish. If the element is rendered by JavaScript after load, wait for it first. Using the same selector in both places is the simplest pattern:

{
  "url": "https://app.example.com/dashboard",
  "wait": {
    "for_selector": "[data-screenshot=\"revenue-chart\"]",
    "for_selector_mode": "visible",
    "timeout": 45
  },
  "capture": {
    "mode": "element",
    "selector": "[data-screenshot=\"revenue-chart\"]",
    "selector_required": true
  }
}

Elements inside a shadow DOM

Web components such as embedded widgets often keep their markup in a shadow root. A normal selector cannot see inside it. Set page.shadow_dom to true so selectors pierce open shadow roots:

{
  "url": "https://example.com/widgets",
  "page": { "shadow_dom": true },
  "capture": {
    "mode": "element",
    "selector": "product-card .price",
    "selector_required": true
  }
}

A closed shadow root (attachShadow({ mode: 'closed' })) cannot be reached by any selector. In that case, capture the host element itself (product-card).

SDK examples

All six SDKs use element(selector) to set element mode and the selector together.

Node.js

import { writeFile } from 'node:fs/promises';
import { Client, TakeOptions } from 'renderscreenshot';

const client = new Client(process.env.RS_API_KEY);

const image = await client.take(
  TakeOptions.url('https://example.com')
    .element('body > div')
    .scale(2)
    .format('png')
);
await writeFile('element.png', image);

Python

import os
from renderscreenshot import Client, TakeOptions

client = Client(os.environ['RS_API_KEY'])

image = client.take(
    TakeOptions.url('https://example.com')
    .element('body > div')
    .scale(2)
    .format('png')
)
with open('element.png', 'wb') as f:
    f.write(image)

Ruby

require 'renderscreenshot'

client = RenderScreenshot.client(ENV.fetch('RS_API_KEY'))

image = client.take(
  RenderScreenshot::TakeOptions
    .url('https://example.com')
    .element('body > div')
    .scale(2)
    .format('png')
)
File.binwrite('element.png', image)

PHP

use RenderScreenshot\Client;
use RenderScreenshot\TakeOptions;

$client = new Client(getenv('RS_API_KEY'));

$image = $client->take(
    TakeOptions::url('https://example.com')
        ->element('body > div')
        ->scale(2)
        ->format('png')
);
file_put_contents('element.png', $image);

Java

Client client = new Client(System.getenv("RS_API_KEY"));

byte[] image = client.take(
    TakeOptions.url("https://example.com")
        .element("body > div")
        .scale(2.0)
        .format("png")
);
Files.write(Path.of("element.png"), image);

Go

client, err := rs.New(os.Getenv("RS_API_KEY"))
if err != nil {
    log.Fatal(err)
}

image, err := client.Take(ctx,
    rs.URL("https://example.com").
        Element("body > div").
        Scale(2).
        Format(rs.FormatPNG),
)
if err != nil {
    log.Fatal(err)
}
os.WriteFile("element.png", image, 0o644)

scale(2) doubles the pixel density of the element image, which keeps text sharp when you place the image on a high-density screen. See retina screenshots for the size cost.

Element, clip or full page?

You know Use Behaviour when the layout changes
A selector for the thing you want capture.mode: element Follows the element
Pixel coordinates only (for example from a design spec) capture.clip Stays on the same rectangle, even if the content moves
Nothing specific, you want the whole page capture.mode: full_page Captures everything

The clip region guide compares element mode and clip side by side.

Troubleshooting

Symptom Likely cause Fix
Request fails with a target_error that names your selector No element matches at capture time Check the selector in DevTools on the live page; wait for it with wait.for_selector if JavaScript creates it
Image shows a different card than expected The selector matches several elements; the first one wins Use an id or a data- attribute, or :nth-of-type()
Sticky header or cookie bar overlaps the top of the element Fixed elements are painted over the element's box Hide them with page.hide: ["header", ".cookie-bar"], or block.cookie_banners: true
Element is empty or shows a skeleton Captured before its data arrived Wait for a child that only exists with data
Element inside an iframe is never found Selectors run in the top-level document only Capture the iframe element itself, or request the iframe's own URL
Element inside a web component is never found It is in a shadow root Set page.shadow_dom: true; for closed roots, capture the host element
Element is narrower or wider than in your browser Responsive CSS reacts to viewport.width Set the viewport width you design for
Rounded corners show a white background The page background is behind the element Use output.transparent: true with PNG or WebP, see transparent backgrounds
Text looks soft on a phone or Retina screen Output at 1x Set viewport.scale to 2

Limits

  • One element per request. To capture several elements of one page, send one request per selector. Each is a separate render and uses a credit.
  • viewport.width accepts 1 to 7,680 px and viewport.scale accepts 1 to 3.
  • The render limit is wait.timeout, 30 seconds by default and 60 seconds at most.

Related in this topic

Was this page helpful?