How to Find Elements Without Specific Attributes in Cheerio
Use CSS attribute negation or Cheerio’s .not() method to select elements missing an attribute, with examples for empty values, scope, and runtime limits.

Use the CSS selector :not([attribute]) to find elements where an attribute is absent. For example, $('li:not([data-id])') selects list items without a data-id attribute. If you already have a Cheerio selection, use .not('[data-id]') to remove elements that have it.
These selectors operate on the HTML tree you pass to Cheerio. Cheerio does not run page JavaScript or render a browser view, so it cannot select attributes added later by client-side code unless those attributes are present in the supplied markup.
1. Select elements without an attribute
Cheerio uses CSS selector syntax for selection. An attribute selector such as [data-id] matches elements on which the attribute exists. Wrapping that selector in :not(...) reverses the test.

const cheerio = require('cheerio');
const html = `
<ul>
<li>No ID</li>
<li data-id="2">Has ID</li>
<li data-id="">Empty ID</li>
</ul>
`;
const $ = cheerio.load(html);
const missingId = $('li:not([data-id])');
console.log(missingId.length); // 1
console.log(missingId.text()); // No ID
The attribute name goes inside square brackets, and the full attribute selector goes inside :not(). Replace data-id with the attribute you are checking. This also works for standard attributes such as href, title, or disabled.
Any element type
To find any element without an attribute, use $(':not([data-test])'). This can return many elements, including structural ancestors and descendants, so constrain the element type or class when possible:
const missingTestAttr = $('button:not([data-test])');
const missingHref = $('a:not([href])');
A selector written as * :not([data-test]) contains a descendant combinator: it selects matching descendants of some element. That is different from selecting every matching element, and the extra space can change the result.
2. Use .not() on an existing selection
Cheerio’s .not() method removes elements matching a selector from the current selection. It is useful when you first narrow the set by a class, tag, or parent, then exclude elements carrying an attribute.
const itemsWithoutTestAttr = $('.item').not('[data-test]');
const linksWithoutTarget = $('a').not('[target]');
console.log(itemsWithoutTestAttr.length);
The equivalent CSS form is $('.item:not([data-test])'). Prefer whichever reads more clearly in context. The selector form expresses the condition in one place; .not() can make a multi-step traversal easier to read.
Filter with JavaScript for custom rules
Use .filter() with a callback when “missing” needs application-specific normalization, such as treating whitespace-only values as empty. The callback receives each element, and the attribute can be read with attr().
const $ = cheerio.load('<div class="item"></div><div class="item" data-code=" "></div>');
const missingOrBlankCode = $('.item').filter((i, el) => {
const value = $(el).attr('data-code');
return value == null || value.trim() === '';
});
This callback treats absent, empty, and whitespace-only values alike. Change the condition if whitespace is meaningful in your data.
3. Require several attributes to be absent
Chain :not() clauses when all listed attributes must be missing. Each adjacent condition narrows the result:
const plainLinks = $('a:not([href]):not([target])');
This returns anchors with neither href nor target. The following comma-separated selector has a different meaning:
const linksMissingEither = $('a:not([href]), a:not([target])');
A comma means “match either selector.” Therefore linksMissingEither can include an anchor that still has one of the attributes, as long as it lacks the other. Use chained negations for “all absent”; use a comma for alternatives.
4. Distinguish absent from empty
Attribute presence is not the same as a non-empty value. The selector [data-id] matches both data-id="2" and data-id="", because the attribute exists in both elements. Accordingly, :not([data-id]) excludes both.
| Markup | [data-id] matches? |
:not([data-id]) matches? |
|---|---|---|
<div> |
No | Yes |
<div data-id="2"> |
Yes | No |
<div data-id=""> |
Yes | No |
If you want elements where the attribute is absent or explicitly empty, combine those alternatives:
const absentOrEmpty = $('[data-id=""], :not([data-id])');
For a particular element type, make both branches explicit, such as div[data-id=""], div:not([data-id]). If your definition also treats whitespace-only strings as empty, use callback filtering so you can trim and normalize the value deliberately.
5. Scope selectors to the right tree
A selector applied to $ searches the loaded document. A selector applied through .find() searches within the current selection. This matters in nested extraction: if a card selection is already scoped to one article, card.find('a:not([href])') only considers anchors inside that card.

const cards = $('.card');
const cardsWithLinkMissingHref = cards.map((i, card) => {
return $(card).find('a:not([href])').length > 0 ? card : null;
});
When a selector returns zero results or unexpectedly many, check the selection root first. Log the length of the parent selection, then test the child selector within that selection. A correct selector against the wrong root still gives the wrong answer.
6. Run a complete Node.js example
Install Cheerio in a Node.js project with npm install cheerio. Save this as find-missing-attribute.js and run it with node find-missing-attribute.js:
const cheerio = require('cheerio');
const html = `
<section>
<article class="item"><a>No href or target</a></article>
<article class="item"><a href="/one">Href only</a></article>
<article class="item"><a target="_blank">Target only</a></article>
<article class="item"><a href="" target="">Both present but empty</a></article>
</section>
`;
const $ = cheerio.load(html);
const noHrefOrTarget = $('a:not([href]):not([target])');
noHrefOrTarget.each((i, el) => {
console.log($(el).text());
});
// If the requirement is absent OR blank after trimming:
const absentOrBlankHref = $('a').filter((i, el) => {
const value = $(el).attr('href');
return value == null || value.trim() === '';
});
console.log('Absent or blank href:', absentOrBlankHref.length);
The first result includes only the anchor with neither attribute. The final filter has a different rule: it accepts missing or blank href values, while not testing target.
7. Understand what Cheerio can inspect
Cheerio parses the markup you provide into a tree and lets you query that tree. It is not a browser: it does not visually render the page, load external resources, or execute JavaScript. If a script adds data-id after the initial document loads, Cheerio will not see it unless your input already includes the updated markup.
Likewise, Cheerio does not apply CSS. An element hidden with display: none remains present in the parsed tree and can still match :not([data-id]). If your task depends on JavaScript-rendered DOM state, use a browser automation or capture workflow to obtain the rendered markup first, then query the resulting HTML. A screenshot alone represents pixels rather than a queryable DOM.
8. cURL, Python, and Node.js with ScreenshotNeo
If the markup you need is only available after a browser loads the page, ScreenshotNeo can capture a page or return page information through its API. Its screenshot response is an image or PDF, so use this when the visual capture is useful; do not treat an image as HTML for Cheerio. See the ScreenshotNeo API documentation for request parameters.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', bytes);
Or skip the browser setup
One API request returns a screenshot. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. ScreenshotNeo is a website screenshot API and MCP server for developers, made by Yorker Media.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Sign up free for 1,000 screenshots a month, no card required.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No elements match | The input HTML lacks the attribute context you expect, the selector is scoped to the wrong root, or a tag/class does not match. | Inspect the supplied markup and check the parent selection length before testing the selector. |
| Elements with an empty attribute are excluded | The attribute exists even though its value is empty. | Use a selector for absent-or-empty values, or callback filtering with an explicit trim rule. |
| Elements added by a script are missing | Cheerio does not execute page JavaScript. | Provide the rendered markup from a browser-based process if runtime DOM changes matter. |
| Hidden elements still match | Cheerio does not apply CSS visibility or layout. | Filter from markup attributes/classes, or use a browser when computed visibility is required. |
| Comma selector returns too many links | A comma forms alternatives rather than requiring every condition. | Chain negations, for example a:not([href]):not([target]). |
| Unexpected behavior after upgrading | Selector support can depend on the installed Cheerio and css-select versions. | Check the project lockfile and installed versions; try the equivalent .not() or callback approach if needed. |
10. Performance, reliability, and cost
For ordinary documents, express simple attribute absence as a selector; it is direct and keeps the rule close to the selection. For custom normalization, use a callback and avoid repeating expensive parsing or broad document scans in a loop. Narrowing to a relevant container before calling .find() also makes the intent clearer and limits the candidate set.
Reliability depends first on the input: Cheerio can only query the HTML tree it receives. When fetching markup yourself, account for network failures, response status, encoding, and pages that deliver different HTML to automated clients. A successful parse does not mean the page was fully rendered. If a browser-rendered capture is needed, ScreenshotNeo bills only clean shots; its responses include X-Page-Verdict and X-Billed headers, and cache hits cost nothing. Review current options and plan details in the documentation before building usage assumptions around a particular capture configuration.
FAQ
Does :not([data-id]) match an element with data-id=””?
No. The attribute is present, so it does not match. Use separate empty-value handling if empty should count as missing.
Should I use :not() or .not()?
Use :not() when defining a selector. Use .not() when excluding matches from an existing Cheerio collection.
Can Cheerio tell whether an element is visible?
Not from CSS rendering. It parses markup and does not calculate layout or apply styles.
Can I use this selector with multiple attributes?
Yes. Chain one negation per attribute when every one must be absent, such as :not([href]):not([target]).


