invalid_selector
invalid_selector means a CSS selector in your request is not valid CSS. The API cannot look for an element when the selector cannot be parsed, so nothing is rendered. The error is about the syntax of the selector, not the page: the same selector fails on every site.
| Field | Value |
|---|---|
| HTTP status | 400 Bad Request |
error.type |
validation_error |
error.code |
invalid_selector |
retryable |
false |
| Credit used | No |
It is part of the error code reference. If the selector is valid but the page has no matching element, you get selectornotfound instead.
Example response
{ "error": { "type": "validation_error", "code": "invalid_selector", "message": "'div[' is not a valid selector", "param": "capture.selector", "retryable": false, "docs": "https://docs.renderscreenshot.com/errors/invalid_selector" }, "request_id": "req_2b81d0c4e7a95f13" }
A selector that only the browser rejects can come back as a target_error (HTTP 422) whose message contains is not a valid selector. Treat both forms the same way: fix the selector and do not retry.
Where selectors appear
These parameters take a CSS selector. Any of them can cause the error:
capture.selector, for element screenshotswait.for_selectorpage.click,page.hideandpage.remove(see page modifications)
Selectors that fail
We ran each selector through document.querySelector in headless Chrome 148. That is the parser the renderer uses.
| Selector | Valid? | Why | Use instead |
|---|---|---|---|
#123 |
No | An ID cannot start with a digit in CSS | #\31 23 or [id="123"] |
.md:flex |
No | :flex is read as a pseudo-class |
.md\:flex |
[data-test=1] |
No | Unquoted attribute values cannot start with a digit | [data-test="1"] |
.price$ |
No | $ is not allowed in a class name without escaping |
[class~="price$"] |
div[ |
No | Unclosed bracket | div[data-id] |
.a,,.b |
No | Empty item in a selector list | .a, .b |
button:has-text("Buy") |
No | Playwright syntax | button[data-action="buy"] |
text=Pricing |
No | Playwright syntax | an id, class or attribute selector |
.btn:contains("Buy") |
No | jQuery syntax, never part of CSS | an attribute selector |
//div[@id="x"] |
No | XPath | div#x |
main >>> .x |
No | Puppeteer-only deep combinator | main .x |
.md\:flex, :has(> img) |
Yes | Escaped colon; :has() is supported |
Tailwind class names are a frequent cause. Classes such as md:flex, w-1/2 and hover:underline need a backslash before : and /.
Escaping in JSON and in URLs
The backslash that CSS needs must survive your transport too:
- JSON body: double it. To send
.md\:flex, write".md\\:flex".JSON.stringifyandjson.dumpsdo this for you. - Query string: percent-encode the whole selector. A raw
#in a URL starts the fragment, so#mainnever reaches the API. Encode it as%23main.
Check selectors before you send them
This script asks Chrome itself whether each selector is valid. It needs Puppeteer but no network access:
// check-selectors-chrome.mjs (Node 22, puppeteer) import puppeteer from 'puppeteer'; const selectors = process.argv.slice(2); const browser = await puppeteer.launch(); let bad = 0; try { const page = await browser.newPage(); for (const s of selectors) { const error = await page.evaluate((sel) => { try { document.querySelector(sel); return null; } catch (err) { return err.message; } }, s); if (error) bad++; console.log(`${error ? 'INVALID' : 'ok '} ${JSON.stringify(s)}${error ? ' ' + error : ''}`); } } finally { await browser.close(); } process.exitCode = bad ? 1 : 0;
node check-selectors-chrome.mjs '#main' '.md:flex' '[data-test="1"]'
The script exits with code 1 if any selector is invalid, so you can run it in CI over the selectors in your config. A lighter check with jsdom was not reliable in our test: jsdom accepted .a,,.b, which Chrome rejects.
Handle it in code
In Python, give the bad selector its own exception, so callers do not put it in a retry queue:
# element_capture.py (Python 3.9+, requests) import os import requests API = os.environ.get("RS_BASE_URL", "https://api.renderscreenshot.com") class BadSelector(Exception): """The selector itself is wrong. Fix it; a retry gives the same result.""" def capture_element(url, selector): res = requests.post( f"{API}/v1/screenshot", headers={"Authorization": f"Bearer {os.environ['RS_API_KEY']}"}, json={"url": url, "capture": {"mode": "element", "selector": selector}}, timeout=(10, 90), ) if res.ok: return res.content error = res.json()["error"] message = error.get("message", "") if error["code"] == "invalid_selector" or "is not a valid selector" in message: raise BadSelector(f"{selector!r}: {message} (request {res.json().get('request_id')})") res.raise_for_status()
Tested on 2026-10-15: the Chrome check with Node 22.18, Puppeteer 24.43.1 and Chrome 148; the Python function with Python 3.14 and requests 2.32.3 against a local mock that returns the API's error envelope.
Related
- selectornotfound: the selector is valid but matches nothing.
- Capture parameters:
capture.mode,selectorandselector_required.