Skip to content
PlaygroundBy 11 min read

CSS selectors for element screenshots: a playground

To screenshot one part of a page, you point at it with a CSS selector. Type one, or pick an example, and see what it matches on this small page and what a capture would return.

Matches 1 element. A capture returns exactly that.

First match: <section id="pricing">

The specimen page

Reports that write themselves

Connect your data and share a live report in minutes.

Start free

Pricing

Free

$0

Pro

$19

Popular
Team

$49

Signups per week
OrderAmount
1042$120
1043$295
Total$415

Made by Harbor

What a capture returns

The crop is the first match at its real size on this page. Nothing you type leaves your browser.

Ten CSS selectors and what each one captures

A CSS selector is a short pattern that points at elements of a page, and an element screenshot is cropped to the first element the pattern matches. These are the ten examples of the playground, with what each matches on the specimen page and how well it survives a change to the site.

Ten example selectors: what each matches on the specimen page, what a capture returns and how robust it is
SelectorMatchesA capture returnsHow robust it is
#pricingMatchesThe pricing section1 elementA capture returnsThe whole pricing sectionSturdyAn id names one element, and it often doubles as a link target, so it is rarely renamed.
.cardMatchesAll three price cards3 elementsA capture returnsThe Free card, because it comes firstFairA class written by a person survives layout changes, but it matches every card.
.card:nth-child(2)MatchesThe second card in the row1 elementA capture returnsThe Pro cardFairIt names a position, not a thing. Add a card in front and it points at a different plan.
[data-plan="pro"]MatchesThe card whose data-plan attribute is pro1 elementA capture returnsThe Pro cardSturdyIt names the thing itself, wherever it sits in the row.
[data-testid="chart"]MatchesThe figure that holds the chart1 elementA capture returnsThe chart with its captionSturdyA test id exists for tools, so nobody changes it for visual reasons.
main > section:first-of-typeMatchesThe first section directly inside main1 elementA capture returnsThe heroFairIt depends on the hero staying first. A banner section added above it takes its place.
table tr:has(td.total)MatchesThe table row that contains the total cell1 elementA capture returnsThe Total rowSturdyIt picks the row by what is inside it, so rows can be added above without breaking it.
.card:not(.featured)MatchesThe cards without the featured class2 elementsA capture returnsThe Free card, the first of the twoFairA fine way to exclude, but it still matches more than one element.
.css-1q2w3eMatchesThe chart drawing, by a generated class name1 elementA capture returnsThe bars without the captionBrittleA build tool made this name from the styles. The next deploy can produce a different one.
body > main > section:nth-child(3) > figureMatchesThe figure, by its full path from the top1 elementA capture returnsThe chart with its captionBrittleFour levels and a position. One new wrapper or one new section anywhere on the path breaks it.

The specimen page, as HTML

The specimen page is 50 elements, and this listing is all of it. The playground runs your selector against exactly this document, so anything you can read here you can select: tags, classes, ids and attributes.

Three details are there on purpose. Only one element has an id. The chart drawing has a class that looks machine-made, css-1q2w3e. And the cards carry a data-plan attribute, the kind of hook a careful site leaves for scripts.

specimen.html
<body>
  <header class="site-header">
    <a class="logo">Harbor</a>
    <nav aria-label="Main">
      <a>Product</a>
      <a>Pricing</a>
      <a>Docs</a>
    </nav>
  </header>
  <main>
    <section class="hero">
      <p class="headline">Reports that write themselves</p>
      <p class="lead">Connect your data and share a live report in minutes.</p>
      <a class="btn">Start free</a>
    </section>
    <section id="pricing">
      <p class="section-title">Pricing</p>
      <div class="cards">
        <article class="card" data-plan="free">
          <strong class="plan">Free</strong>
          <p class="price">$0</p>
        </article>
        <article class="card featured" data-plan="pro">
          <strong class="plan">Pro</strong>
          <p class="price">$19</p>
          <span class="badge">Popular</span>
        </article>
        <article class="card" data-plan="team">
          <strong class="plan">Team</strong>
          <p class="price">$49</p>
        </article>
      </div>
    </section>
    <section class="stats">
      <figure data-testid="chart">
        <svg class="css-1q2w3e" role="img" aria-label="Signups per week">
          <rect class="bar" />
          <rect class="bar" />
          <rect class="bar" />
          <rect class="bar" />
        </svg>
        <figcaption>Signups per week</figcaption>
      </figure>
      <table class="orders">
        <thead>
          <tr>
            <th>Order</th>
            <th>Amount</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>1042</td>
            <td>$120</td>
          </tr>
          <tr>
            <td>1043</td>
            <td>$295</td>
          </tr>
          <tr>
            <td>Total</td>
            <td class="total">$415</td>
          </tr>
        </tbody>
      </table>
    </section>
  </main>
  <footer class="site-footer">
    <p>Made by Harbor</p>
  </footer>
</body>

CSS selector cheat sheet for picking one element

This CSS selector cheat sheet lists the patterns that matter for pointing at one element, grouped by kind. Every example works on the specimen page, so you can paste it into the playground. Meanings and browser support are from MDN.

CSS selectors by kind, with meaning, an example and browser support
PatternMeaningExample on the specimen page
Id, class and typeEvery browser[1]
#idId selector. The element whose id attribute has this value.#pricingthe pricing section
.classClass selector. Every element that has this class..cardthree cards
tagType selector. Every element with this tag name.tablethe orders table
tag.classCompound selector. Conditions written without a space must all hold for the same element.article.featuredthe Pro card
AttributeAcross browsers since July 2015[2]
[attr]Has the attribute. Every element that has the attribute, with any value.[data-plan]three cards
[attr="value"]Exact value. The attribute value is exactly this.[data-plan="team"]the Team card
[attr^="value"]Starts with. The attribute value begins with this.[class^="css-"]the chart drawing
[attr$="value"]Ends with. The attribute value ends with this.[class$="header"]the page header
[attr*="value"]Contains. The attribute value contains this somewhere.[aria-label*="Signups"]the chart drawing
[attr~="value"]Has the word. The value is a list of words separated by spaces, and one of them is exactly this.[class~="featured"]the Pro card
Descendant and childEvery browser[1]
A BDescendant combinator. B anywhere inside A, at any depth.nav athree links
A > BChild combinator. B that is a direct child of A.main > sectionthree sections
SiblingEvery browser[1]
A + BNext-sibling combinator. B that comes immediately after A, with the same parent.figure + tablethe orders table
A ~ BSubsequent-sibling combinator. Every B that comes after A, with the same parent..hero ~ sectionpricing and stats
PositionAcross browsers since July 2015; the "of selector" form is newer[3]
:nth-child(n)Nth child. The element that is the nth child of its parent. Counting starts at 1 and includes siblings of every type..card:nth-child(2)the Pro card
:nth-child(n of S)Nth match. The nth child among only those siblings that match the selector S..card:nth-child(2 of :not(.featured))the Team card
:first-of-typeFirst of its type. The first sibling with its tag name.main > section:first-of-typethe hero
:last-childLast child. The element that is the last child of its parent.tbody tr:last-childthe Total row
HasAcross browsers since December 2023[4]
A:has(B)Relational pseudo-class. A, when it contains at least one B. It selects the parent, not the child.section:has(table)the stats section
A:has(> B)Has a direct child. A, when B is one of its direct children.article:has(> .badge)the Pro card
NotAcross browsers since July 2015[6]
A:not(B)Negation pseudo-class. A, unless it also matches B..card:not(.featured)the Free and Team cards
IsAcross browsers since January 2021[5]
:is(A, B)Matches-any pseudo-class. An element that matches any selector in the list. It shortens long lists.:is(figure, table)the chart and the table

Is :has() safe to use?

Yes, in current browsers. MDN lists :has() as available across browsers since December 2023.[4] It is the only selector that picks an element by what is inside it, which is what a capture often needs: the card that contains the badge, the row that contains the total. One limit: a :has() cannot be nested inside another :has().[4]

Which CSS selectors are robust, and which break?

A robust selector keeps pointing at the same thing after the site changes. The scale below orders five kinds of selector from brittle to sturdy by one question: how much reason does the site have to keep this hook as it is? It is reasoning, not a measurement.

  1. Most brittle

    Generated class names

    .css-1q2w3e

    CSS-in-JS libraries and build tools derive these names from the styles, so a change to the styling or a new build can rename them.

  2. 2 of 5

    Deep positional chains

    body > main > section:nth-child(3) > figure

    Every level and every number is a promise about the layout, and one new wrapper or sibling breaks the chain.[11]

  3. 3 of 5

    Meaningful class names

    .pricing-table

    A person chose the name to describe the thing, so it survives small layout changes, but a redesign can rename it and several elements can share it.[13]

  4. 4 of 5

    Ids

    #pricing

    An id is meant to name one element in the document and is often the target of links, which gives the site a reason to keep it.

  5. Sturdiest

    Data attributes and test ids

    [data-testid="chart"]

    They exist for scripts and tests, not for styling, so a visual redesign has no reason to touch them.[11]

“CSS and XPath are not recommended as the DOM can often change leading to non resilient tests.”
Playwright documentation, Locators[11]

Playwright, the browser automation library, gives this advice for tests, and it carries over to screenshots: a capture that runs every night is a test of your selector. The same page calls test ids “the most resilient way of testing”.[11] Its best-practices page shows why a styling class is a weaker hook: “Should the designer change something then the class might change”.[13]

On a site you do not control you take what exists. Look first for an id, a data- attribute or an aria-label, then for a class that describes the thing, and use positions only as a last resort.

How to find a CSS selector in 20 seconds

Finding the CSS selector of an element takes six steps in the developer tools of Chrome or Firefox: inspect, climb, copy, simplify, count, check. The copied selector is a starting point, and the simplifying is what makes it last.

  1. Inspect the part you want

    Right-click it on the page. The developer tools open with that element selected in the page tree.[14][18]

    Chrome
    Inspect
    Firefox
    Inspect Element
  2. Climb to the element that wraps all of it

    The click usually lands on something small, such as one line of text. Move up the tree, one parent at a time, until the highlight on the page covers the whole chart, card or table.

  3. Copy a selector

    Right-click the element in the tree and copy its selector. In Firefox, choose CSS Selector and not CSS Path: the path is the full chain from the top of the page.[15][17]

    Chrome
    Copy > Copy selector
    Firefox
    Copy > CSS Selector
  4. Simplify what you copied

    Look at the element for an id, a data attribute or a class that describes it, and use that alone. Delete position numbers and generated class names. #pricing beats #app > div:nth-child(2) > section.css-x1y2z3.

  5. Count the matches in the console

    Open the Console and run document.querySelectorAll("#pricing").length. You want 1. A result of 0 means a typo or a late element, and an invalid selector throws a SyntaxError. In Chrome, $$("#pricing") is a shorter way to get the matches.[8][16]

  6. Check that the first match is the right one

    When the count is more than 1, run document.querySelector(".card"). It returns the first match in document order, which is the element a capture takes. Narrow the selector until that is the one you want.[7]

Console
// 3: too many
document.querySelectorAll('.card').length

// 1: good
document.querySelectorAll('#pricing').length

// The first match: what a capture takes
document.querySelector('.card')

// SyntaxError: not valid CSS
document.querySelectorAll('.card:nth(')

// Chrome shorthand, returns an array
$$('[data-plan="pro"]')
Steps 5 and 6 in the browser console, with the selectors of the specimen page. querySelectorAll returns every match in document order and throws a SyntaxError for a selector that is not valid CSS.[8] querySelector returns the first match, or null.[7] Chrome also lets you search the page tree by selector: press Control+F or Command+F in the Elements panel.[14]

A copied selector is often the brittle kind

The developer tools write a selector that is unique today, which often means a chain of positions. Firefox even offers two: CSS Selector, which “uniquely selects the element”, and CSS Path, “the full path to the element”.[17] Take the short one and shorten it again.

What goes wrong with element screenshots, and the fix

An element screenshot fails in six recognisable ways, and each one has a cause you can name. Find the symptom, read the cause, apply the fix. In the drawings the blue frame is the crop and the grey shapes are the page.

  • The selector matches nothing

    Causes

    • The element is added by a script after the page has loaded.
    • The element is inside an iframe. An iframe holds a separate document, and a selector run on the outer page does not look into it.
    • The element is inside a shadow DOM, the private tree of a web component. document.querySelectorAll does not find elements in there.

    Fix

    Wait for the element before capturing. For an iframe, capture the iframe element itself, or open the address the frame loads and capture that page. For shadow DOM, use a tool whose selectors pierce open shadow roots, as Playwright does, or select the host element.[9][10][12]

  • It captures the wrong element

    Cause

    • The selector matches several elements and the capture takes the first one in document order, which may be a hidden template, a mobile menu or an earlier card.

    Fix

    Count the matches in the console, then narrow the selector with a parent (#pricing .card), an attribute ([data-plan="pro"]) or :has().[7]

  • The element is cut off at the bottom

    Cause

    • The element is taller than the browser window, and the tool captured only the part that was on screen.

    Fix

    Use a tool that captures the element from the whole document and not from the visible window. If yours does not, make the window taller than the element.

  • A header or banner covers part of it

    Cause

    • A sticky header, a cookie banner or a chat bubble is drawn on top of the page, and it was sitting over the element at the moment of capture.

    Fix

    Hide the overlay before the capture with display: none, by its own selector. Hiding is safer than scrolling: the overlay follows the scroll.

  • The element is there but empty inside

    Cause

    • Images, a chart or a map inside the element load only when the element scrolls near the screen, or draw themselves a moment after the page loads.

    Fix

    Wait for something that exists only once the content has loaded, such as #chart canvas or .map img, and add a short pause for drawing animations. Scroll through the page first if the content loads on scroll.

  • The capture fails although the element exists

    Cause

    • The element is hidden (display: none, a closed menu or tab), or it has no size because everything inside it is positioned outside its box. There is no rectangle to capture.

    Fix

    Open it first with a click, or select the visible element inside or around it. In the console, getBoundingClientRect() on the element shows a width or height of 0 when this is the cause.

Selector recipes for six common targets

These six recipes cover the parts of a page people capture most: a chart, a pricing table, a single post, a map, a code block and a table. The selectors are typical patterns to adapt, sturdiest first, and the options are those of CurlShot.

  • A chart

    • [data-testid="revenue-chart"]
    • #revenue-chart
    • figure:has(canvas)

    Select the box around the chart, not the canvas or svg inside it, so the title and legend come along. Wait for the drawing element to exist, and stop animations so bars are not caught half grown.

    • wait_for_selector=figure canvas
    • reduced_motion=true
    • delay=1
  • A pricing table

    • #pricing
    • [data-section="pricing"]
    • section:has(.price)

    The window width decides whether the cards sit side by side or stack, so set it. Hide the cookie banner and the chat bubble, which like to sit over the bottom of a pricing section.

    • viewport_width=1280
    • block_cookie_banners=true
    • block_chats=true
  • A single post or comment

    • #comment-4821
    • [data-post-id="4821"]
    • article:first-of-type

    A permalink that ends in #comment-4821 points at an element with that id, so the part after the # is your selector. A double pixel density keeps small text sharp.

    • device_scale_factor=2
    • hide_selectors=header,.reply-form
  • A map

    • #map
    • [aria-label="Map"]
    • section:has(> .map)

    Map tiles are images that arrive after the page has loaded. Wait for the network to go quiet and add a pause, or the capture shows grey squares.

    • wait_until=networkidle
    • delay=2
  • A code block

    • pre
    • article pre:nth-of-type(2)
    • pre:has(code.language-js)

    An element capture shows the box of the element, so a long line that scrolls sideways inside the block is cut at the edge. Let it wrap with a line of CSS, and add padding the same way.

    • styles=pre{white-space:pre-wrap;padding:24px}
    • device_scale_factor=2
  • A full table

    • #results
    • table.results
    • table:has(caption)

    Select the table itself. When it sits in a box that scrolls, only the visible rows are painted, so remove the height limit of that box with CSS.

    • styles=.table-wrap{max-height:none;overflow:visible}
    • hide_selectors=header

Element screenshots with the CurlShot API

In CurlShot the option is selector. It takes one CSS selector of up to 1000 characters and returns a PNG, JPEG or WebP of the first element that matches, exactly as big as the element. The element does not have to be on the first screen, and it may be taller than the window.

There is no padding option and no scroll-into-view option. Scrolling to the element is automatic, and space around it comes from styles: padding added to the element makes its box, and so the image, bigger.

Try it without code on the element screenshot tool, or read capturing an element and waiting and timing in the docs.

An element capture
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com&selector=h1&wait_for_selector=h1&hide_selectors=.cookie-bar,%23chat&device_scale_factor=2" \
  --output heading.png

In a web address # is written %23, or everything after it is dropped before the request is sent. Here %23chat is #chat.

What the selector option does, case by case

What happens to an element capture in each situation, with the error code when it fails
SituationWhat happensError
One element matchesAn image exactly as big as the element, in page pixels times device_scale_factor.An image
Several elements matchThe first one in document order is captured. The others are ignored.An image
Nothing matchesThe capture waits up to 3 seconds for the element to appear, then fails. The request is not counted against the quota.422 selector_not_found
The element is hidden or has no sizeThe same error, because there is no rectangle to capture.422 selector_not_found
The selector is not valid CSSThe request is refused and names the selector.400 invalid_options
The element is below the first screenIt is scrolled into view and captured. No option is needed.An image
The element is taller or wider than the windowIt is captured whole, from the document and not from the window.An image
The element is inside an open shadow DOMIt is found: selector, wait_for_selector and click pierce open shadow roots. hide_selectors does not.An image
The element is inside an iframeIt is not found. Capture the iframe element, or the address the frame loads.422 selector_not_found
The element is very large at a high pixel densityThe request fails. Lower device_scale_factor.413 content_too_large
selector together with clip_*, format=pdf or a video formatThe request is refused. An element capture is an image: PNG, JPEG or WebP.400 invalid_options

Options that work with selector

Eight options of CurlShot change what an element capture shows. They run in a fixed order, so a click happens after the overlays are hidden, and the element is measured last.

  1. wait_for_selector
  2. delay
  3. styles
  4. hide_selectors
  5. scripts
  6. click
  7. selector
wait_for_selector
Waits until an element matching this selector is visible, for as long as timeout allows (30 seconds by default).
hide_selectors
Hides every element that matches any selector in the list (up to 50) with display: none before the capture.
click
Clicks the first element that matches this selector before the capture, to open a tab or a menu.
styles
Adds your CSS to the page. This is how to add padding around the element: there is no padding option.
delay
Waits this many seconds more, up to 30, for charts and animations that draw late.
full_page
With selector, scrolls through the page first so content that loads on scroll is there, then captures only the element.
device_scale_factor
Pixel density from 1 to 3. At 2, an element 600 pixels wide gives an image 1200 pixels wide.
omit_background
Keeps areas the page does not paint transparent, in PNG and WebP.

Selector and capture glossary

CSS selector
A short pattern that points at elements of a web page, such as #pricing or .card. It is the language stylesheets use to say which elements a rule applies to.
Element screenshot
A screenshot cropped to the rectangle of one element of a page, instead of the window or the whole page.
Bounding box
The smallest rectangle that contains an element as drawn, including its padding and border but not its margin. It is what an element screenshot crops to.
Document order
The order in which elements appear in the HTML source, parents before their children. The first match of a selector is the first one in this order.
Combinator
The sign between two parts of a selector that says how they are related: a space (inside), > (direct child), + (next sibling) or ~ (later sibling).
Pseudo-class
A keyword that starts with a colon and selects by state or position instead of by name, such as :first-child, :not() or :has().
Data attribute
An attribute whose name starts with data-, which a site adds to carry its own information. data-testid is a common one, added so tests can find an element.
Shadow DOM
A tree of elements attached to one element and hidden from the scripts and styles of the page around it. Web components use it to keep their insides private.
Iframe
An element that shows another document inside the page, such as an embedded video, map or payment form. Its content is a separate document with its own elements.
Viewport
The part of the page the browser window shows at one time. Its width and height are set when a capture is requested.

Questions people ask

How do I take a screenshot of one element on a web page?

Find a CSS selector for the element (its id, a data attribute or a class), then pass it to a screenshot tool that supports element capture. In CurlShot, add selector to the request, for example selector=%23pricing, and the image contains only that element.

What happens if a CSS selector matches more than one element?

An element capture takes the first match in document order, which is the element document.querySelector returns. The other matches are ignored. To pick a later one, narrow the selector with a parent, an attribute, :nth-child() or :has().

Which CSS selector is the most reliable for screenshots?

A data attribute such as [data-testid="chart"] or an id such as #pricing. Both name the thing itself and have no reason to change when the page is restyled. Generated class names like .css-1q2w3e and long chains of positions are the least reliable.

Can a CSS selector reach inside an iframe or a shadow DOM?

Not inside an iframe: its content is a separate document, so capture the iframe element or the address it loads. Inside a shadow DOM, document.querySelectorAll finds nothing, but tools built on Playwright pierce open shadow roots. CurlShot does, for selector, wait_for_selector and click.

Is :has() safe to use in a selector?

Yes in current browsers. MDN lists :has() as available across browsers since December 2023. It selects an element by what it contains, for example tr:has(td.total) for the table row with the total cell. It cannot be nested inside another :has().

How do I get the CSS selector of an element in Chrome or Firefox?

Right-click the element and choose Inspect. In the page tree, right-click the element and choose Copy > Copy selector in Chrome or Copy > CSS Selector in Firefox. Then shorten the result to the id, data attribute or class that identifies the element.

Why does my element screenshot fail although the element is on the page?

Usually the element appears after the page has loaded, sits inside an iframe, or is hidden or has no size at the moment of capture. Wait for it to be visible first. In CurlShot that is wait_for_selector with the same selector.

Sources

  1. [1]MDN Web Docs, CSS selectors and combinators. Checked .
  2. [2]MDN Web Docs, Attribute selectors. Checked .
  3. [3]MDN Web Docs, :nth-child(). Checked .
  4. [4]MDN Web Docs, :has(). Checked .
  5. [5]MDN Web Docs, :is(). Checked .
  6. [6]MDN Web Docs, :not(). Checked .
  7. [7]MDN Web Docs, Document: querySelector() method. Checked .
  8. [8]MDN Web Docs, Document: querySelectorAll() method. Checked .
  9. [9]MDN Web Docs, Using shadow DOM. Checked .
  10. [10]MDN Web Docs, HTMLIFrameElement: contentDocument property. Checked .
  11. [11]Playwright, Locators. Checked .
  12. [12]Playwright, Other locators: CSS locator. Checked .
  13. [13]Playwright, Best practices. Checked .
  14. [14]Chrome for Developers, Get started with viewing and changing the DOM. Checked .
  15. [15]Chrome for Developers, What's new in DevTools (Chrome 72): Copy JS path. Checked .
  16. [16]Chrome for Developers, Console utilities API reference. Checked .
  17. [17]Firefox Source Docs, Examine and edit HTML. Checked .
  18. [18]Firefox Source Docs, Open the Inspector. Checked .

Screenshot any page in one step

Paste an address into CurlShot and download the screenshot. A free account gives you 100 screenshots every month. No card needed.