How to Find HTML Elements by Multiple Tags with Cheerio
Select several HTML tag types in one Cheerio query with a comma-separated CSS selector. See runnable examples, scoped searches, security tips, and fixes for common mistakes.

To find several HTML tag types with Cheerio, pass a comma-separated CSS selector to the query function: $('h1, h2, p'). The commas mean “match any of these selectors,” so this returns all matching h1, h2, and p elements in document order. [Cheerio selector guide]
First load the markup with cheerio.load(); it returns the document-bound $ function used for queries. [Cheerio introduction]
const cheerio = require('cheerio');
const html = `
<article>
<h1>Page title</h1>
<p>Introduction</p>
<h2>Details</h2>
<div>Other content</div>
</article>
`;
const $ = cheerio.load(html);
const matches = $('h1, h2, p');
matches.each((_, element) => {
console.log(element.tagName, $(element).text().trim());
});
This article shows the selector syntax, how to scope it, how to filter and transform results, and how to avoid common errors when using it in a scraper.
1. Install Cheerio and load HTML
In a Node.js project, install Cheerio with your package manager. This example uses npm:
npm install cheerio
Save the following as index.js and run it with Node.js:
const cheerio = require('cheerio');
const html = '<h1>Title</h1><p>Intro</p><h2>Details</h2>';
const $ = cheerio.load(html);
$('h1, h2').each((index, element) => {
console.log(`${index}: <${element.tagName}> ${$(element).text()}`);
});
Cheerio parses the supplied markup and gives you a query function attached to that document. A selector is evaluated against the loaded document; it does not fetch a URL for you. If your input comes from a website, obtain the HTML separately and then pass the response text to cheerio.load().
2. Understand comma-separated selectors
A comma creates a selector list. Each entry is an alternative, and an element matching any entry is included. For example:

const headings = $('h1, h2, h3');
const textBlocks = $('p, blockquote, li');
These are not requests for a single element to be three tags at once. An element has one tag name, and the selector list collects elements matching each alternative. Cheerio’s official selector guide uses $('h1, h2') to demonstrate selecting multiple tag types. [Selector guide]
| Selector | Meaning |
|---|---|
h1, h2 |
An h1 or an h2 |
p.selected |
A paragraph that also has class selected |
h1, p.selected |
An h1, or a selected paragraph |
article h2, article p |
An h2 or p descendant of an article |
Spaces and punctuation matter. h1, h2 is a list of alternatives; h1 h2 means an h2 nested inside an h1, which is usually not the intent.
3. Select multiple tags inside a specific section
Use a contextual selection when the document contains matching tags in headers, navigation, footers, or unrelated articles. The query function accepts a selector and context, and .find() searches descendants of the current selection. [Cheerio selecting] [Cheerio find API]

const articleContent = $('.article').find('h2, p');
articleContent.each((_, element) => {
console.log(element.tagName, $(element).text().trim());
});
You can also pass a context to the query function:
const article = $('.article').first();
const content = $('h2, p', article);
Choose the narrowest stable container that represents the content you need. If no elements match the container selector, the resulting selection is empty, and the descendant query also returns no elements.
4. Read tag names, text, and attributes
Cheerio selections are collections. Iterate over them with .each(); its callback receives an index and the underlying element. Wrap the element with $(element) to use Cheerio methods such as .text() and .attr().
const fields = $('h1, h2, a');
const rows = [];
fields.each((_, element) => {
const node = $(element);
rows.push({
tag: element.tagName,
text: node.text().trim(),
href: node.attr('href') ?? null
});
});
console.log(rows);
Not every selected tag has an href, so a missing attribute yields undefined; the example converts that to null for consistent output. .text() returns the combined text of an element’s descendants. If whitespace is meaningful, avoid trimming or normalize it deliberately rather than assuming all text should be collapsed.
5. Filter a broad tag list
Use the selector for structural categories, then apply a predicate when the condition is easier to express as JavaScript. For example, find headings and paragraphs, then keep only non-empty text:
const useful = $('h1, h2, p').filter((_, element) => {
return $(element).text().trim().length > 0;
});
For a small, fixed set of tag names, a comma selector is direct and readable. If the set is assembled dynamically, build it only from trusted, validated tag names; arbitrary selector fragments can change the selector’s meaning or cause parsing errors.
6. Keep untrusted values out of selector strings
Do not interpolate attacker-controlled values directly into CSS selector syntax. A value containing quotes or selector punctuation can alter what gets matched. Cheerio’s security guide demonstrates this risk for attribute selectors and recommends selecting with fixed syntax, then comparing the attribute value as data. [Cheerio security guide]
Risky pattern:
// Avoid if userValue is untrusted:
const found = $(`[data-name="${userValue}"]`);
Safer pattern:
const found = $('[data-name]').filter((_, element) => {
return $(element).attr('data-name') === userValue;
});
The selector is fixed, and the supplied value is checked as a string. If you need a dynamic selector for a controlled use case, validate or escape the value using an approach supported by your installed selector stack, and review the library guidance for that version.
7. Complete runnable example: extract a content outline
This complete script loads a small HTML fragment, selects heading tags in one query, and prints a JSON outline. Save it as outline.js after installing Cheerio:
const cheerio = require('cheerio');
const html = `
<main class="article">
<h1>Cheerio selectors</h1>
<p>A short introduction.</p>
<h2>Selector lists</h2>
<p>Commas separate alternatives.</p>
<h3>Context</h3>
<h2>Security</h2>
</main>
`;
const $ = cheerio.load(html);
const outline = $('.article').find('h1, h2, h3').map((_, element) => ({
level: element.tagName,
text: $(element).text().trim()
})).get();
console.log(JSON.stringify(outline, null, 2));
Using .get() at the end converts the Cheerio collection produced by .map() into a regular JavaScript array. Keep the parent context in the query when you want the outline for one article rather than every heading in the parsed page.
8. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| No results | The markup does not contain those tags, or the content is in a different subtree. | Inspect the HTML passed to load(); test one selector at a time and check the context element. |
| Only one tag type appears | The comma or a tag name is missing, or the selector was written as a descendant relationship. | Use h1, h2 for alternatives; use a space only when expressing nesting. |
| Unexpected matches from the whole page | The query is global to the loaded document. | Scope with $('.article').find('h1, h2'). |
| Selector parse error | A malformed selector or untrusted interpolated value introduced special syntax. | Check commas, quotes, brackets, and parentheses. Use fixed selectors and compare data in .filter(). |
| Text includes nested labels or whitespace | .text() combines descendant text and preserves some whitespace. |
Trim for display or traverse the exact child nodes if the extraction needs finer control. |
Cannot find module 'cheerio' |
Cheerio is not installed in the project directory used to run the script. | Run npm install cheerio in that project and execute the script there. |
| Browser-rendered content is missing | The input HTML was fetched before client-side code rendered the content. | Cheerio parses supplied markup; obtain rendered HTML from an appropriate browser capture workflow before parsing. |
9. Performance, reliability, and cost
A comma-separated query expresses several alternatives in one selector and keeps extraction code compact. For ordinary documents, first make the selector as specific as the task requires: a subtree query avoids collecting unrelated nodes and simplifies downstream filtering. When processing many documents, parse each document once, reuse its $ function for related queries, and avoid repeatedly loading the same HTML.
Reliability depends on the input and the structure you target. Real pages can change their markup, omit optional tags, or render content only in a browser. Handle empty selections, use a stable content container, and make extraction tolerate missing attributes. Pin and review the Cheerio version used by your project; the selector API and engine support can vary across releases, so consult documentation corresponding to your installed version. The official guide documents CSS selector syntax and comma-separated selection. [Cheerio selector guide]
Cheerio is a library used in your Node.js process; the selector itself has no per-query service price. Your practical costs are the compute and network work involved in obtaining and processing source HTML, along with the time spent maintaining selectors as page structures change. If the goal is a visual screenshot rather than structured text extraction, parsing HTML is the wrong output path: a screenshot service captures a rendered page.
10. Or skip the browser setup
If your task is to obtain a visual image of a page rather than parse elements from source HTML, ScreenshotNeo is a website screenshot API and MCP server. A GET request returns a PNG, JPEG, WebP, or PDF, while the Cheerio examples above remain the right approach for extracting DOM text and attributes. See the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 shots each month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
11. Short FAQ
Can I select tags regardless of capitalization?
Use the tag names as they appear in normal HTML selectors, such as h1 and p. If parsing unusual markup, check the behavior of the Cheerio version in your project.
Does selecting multiple tags change their order?
The resulting selection follows document order; it is not grouped by the order in which selector alternatives are written.
Can I select tags and classes together?
Yes. A selector list can mix selector forms, for example h1, p.summary, li. Each comma-separated entry is an independent alternative.
Does Cheerio execute page JavaScript?
Cheerio parses the markup you provide. It does not by itself run a site’s browser scripts; supply HTML that already contains the content you intend to query.


