ScreenshotNeo

BlogGuides

CSS Selectors: How to Find and Target Elements

Learn CSS selector syntax for finding and styling elements, then use querySelector() and querySelectorAll() safely in JavaScript.

By the ScreenshotNeo team4 October 20268 min read

A CSS selector is a pattern that matches elements in a document. In a stylesheet, it determines which elements receive a rule; in JavaScript, the same selector syntax lets you find elements with querySelector() and querySelectorAll().

Start with a type, class, ID, or attribute selector. Add conditions to match one element more precisely, or use combinators to describe its relationship to other elements. Use querySelector() for the first match and querySelectorAll() when you need every match.

1. CSS selector syntax at a glance

A selector can be a simple pattern, several conditions on one element, or multiple patterns connected by relationships. The W3C describes the basic idea simply: “A selector represents a structure.” See the W3C Selectors Level 4 draft and MDN’s CSS selectors reference.

What you want Selector form Example
Elements of a type Type selector button
Any element Universal selector *
An element with a class Class selector .notice
An element with an ID ID selector #main
An element with an attribute Attribute selector input[type="email"]
One element meeting several conditions Compound selector button.primary[disabled]
A descendant somewhere inside another element Descendant combinator (space) article p
A direct child Child combinator ul > li
The next sibling Adjacent sibling combinator h2 + p
A later sibling General sibling combinator h2 ~ p
An element in a state or structural position Pseudo-class input:focus, li:first-child
A styled part of an element Pseudo-element p::first-line
Either of several alternatives Selector list h1, h2

2. Build a selector from the element outward

Suppose the page contains a primary submit button inside a checkout form. Begin with what identifies the element, then add only the conditions needed to distinguish it:

button
button.primary
button.primary[type="submit"]
form.checkout button.primary[type="submit"]

The first three lines are compound selectors: each condition applies to the same element. The last is a complex selector: it matches a qualifying button that is a descendant of a qualifying form.

Type, universal, class, and ID selectors

  • button matches button elements.
  • * matches any element and is most useful as part of a larger pattern.
  • .notice matches elements whose class list contains notice.
  • #main matches an element with ID main. IDs are intended to be unique, so prefer classes when selecting a reusable group.

Classes may contain multiple tokens. .card.featured requires both classes on the same element; .card .featured instead looks for a descendant with class featured inside an element with class card.

Attribute selectors

Attribute selectors match an attribute’s presence or value. For example, [disabled] matches elements with a disabled attribute, while [type="email"] matches an exact value. CSS also supports operators for values that equal, begin with, end with, or contain a string. Check the MDN attribute selector reference for the full operator syntax and modifiers.

input[required]
a[href^="https://"]
img[alt]
[data-state="open"]

Pseudo-classes and pseudo-elements

A pseudo-class adds a condition, often a state or structural position: a:hover, input:focus, or li:first-child. A pseudo-element describes a stylable part, such as p::first-line. The double-colon form is the modern notation for pseudo-elements. A pseudo-element can be styled in CSS, but it is not an ordinary DOM element that JavaScript can retrieve.

3. Use combinators to describe relationships

Combinators connect selector parts. A space means descendant; it can match at any depth. Use > for a direct child, + for the immediately following sibling, and ~ for a later sibling with the same parent.

article p       /* any paragraph inside an article */
article > p     /* paragraph that is a direct child of an article */
h2 + p           /* paragraph immediately after an h2 */
h2 ~ p           /* any later paragraph sibling of an h2 */

Whitespace matters: nav a and nav > a express different relationships. If a selector returns too many elements, check whether a descendant combinator should be a child combinator, or add a class or attribute condition.

4. Find elements with JavaScript

These methods are available on document and on elements. The selector must be valid CSS syntax.

// Get the first matching element, or null if there is no match.
const firstButton = document.querySelector("button.primary");

// Get every matching element in a static NodeList.
const buttons = document.querySelectorAll("button");

// Search within a particular element's descendants.
const panel = document.querySelector(".panel");
const nestedLink = panel?.querySelector("a[href]");

if (firstButton) {
  firstButton.disabled = true;
}

buttons.forEach((button) => {
  button.setAttribute("aria-pressed", "false");
});

querySelector() returns the first matching Element or null. querySelectorAll() returns a static NodeList of all matches; it does not update automatically when the DOM changes. An element-scoped query searches its descendants, not the element itself. See MDN: Document.querySelector() and MDN: Document.querySelectorAll().

Choose the method by the result you need

Method Use when Result
querySelector(selector) You need one match First matching element or null
querySelectorAll(selector) You need every current match Static NodeList, possibly empty
element.querySelector(selector) You want to search under a known container First matching descendant or null

If the page has duplicate IDs, a first-match query returns the first one in document order. Fix duplicate IDs in markup when possible; otherwise use a selector that identifies the intended context.

5. Construct selectors safely from dynamic values

HTML class and ID values are not required to follow CSS identifier rules. If a value comes from data and includes punctuation or other special characters, directly concatenating it into a selector can produce an invalid selector or change what the selector means. Keep the selector structure fixed and escape the dynamic identifier with CSS.escape().

const rawId = "item:42";
const element = document.querySelector(`#${CSS.escape(rawId)}`);

const rawClass = "status/ready";
const matching = document.querySelector(`.${CSS.escape(rawClass)}`);

For a dynamic attribute value, avoid interpolating arbitrary text as selector syntax. Prefer selecting a stable set and comparing the attribute value in JavaScript, or carefully escape according to the selector context. MDN documents CSS.escape().

6. Selector lists, specificity, and newer features

Commas create alternatives: h1, h2 matches headings of either type. Conditions without a combinator apply to one element: button.primary[disabled] must be a button with both the class and attribute.

In stylesheets, specificity affects which declaration wins when rules conflict. The modern :is() pseudo-class takes the specificity of its most specific argument; :where() contributes zero specificity. These details matter when a selector is used for styling, but not when JavaScript is simply locating matches. See the Selectors specification.

Selectors Level 4 is a W3C Working Draft dated 22 January 2026. A syntax feature appearing in a draft does not guarantee support in every browser, automation runtime, or embedded webview. Check support for the exact feature and target environments before relying on newer selectors in production.

7. Verify a selector in the browser

  1. Inspect the target element in browser developer tools and look for stable classes, IDs, or attributes.
  2. Try the selector in the console with document.querySelectorAll("your-selector").
  3. Check the match count and inspect the returned elements. If it is zero, verify spelling, nesting, and whether the target has loaded.
  4. Use a container-scoped query when the same child pattern appears in multiple parts of the page.
  5. Prefer stable application-owned attributes over generated class names that may change between builds.

8. Common errors and fixes

Symptom Likely cause Fix
SyntaxError from a query The selector string is not valid CSS, often due to missing quotes/brackets or an unescaped dynamic ID/class. Check the selector syntax and escape dynamic identifiers with CSS.escape().
querySelector() returns null No current element matches; the code may run before rendering or the selector may be wrong. Confirm the selector in developer tools and run after the relevant DOM is present. Handle the no-match case.
The query finds the wrong element The selector is too broad, or a space matches descendants at any depth. Add a distinguishing class/attribute or use > when only a direct child should match.
Only one result is processed querySelector() returns only the first match. Use querySelectorAll() and iterate the resulting NodeList.
A pseudo-element query returns no element Pseudo-elements are styling constructs, not DOM elements returned by these methods. Use the pseudo-element in CSS; query the originating element if script access is needed.
Results are stale after a DOM update querySelectorAll() returns a static NodeList. Run the query again after the update, or use a MutationObserver if changes must be observed continuously.
A selector works locally but fails in a target browser A newer selector feature may not be supported in that browser or runtime. Check feature-specific compatibility and use a simpler selector or JavaScript filtering as a fallback.

9. Performance and reliability

For ordinary interface code, prioritize selectors that clearly identify the intended element and are stable across renders. Avoid repeatedly querying in tight loops when one query followed by iteration will do. Store a result when you need to reuse it, and query again when the DOM may have changed because query results do not all behave the same: querySelectorAll() is a static snapshot.

Do not assume a particular selector is faster based on its shape alone; the practical impact depends on the page, browser, and how often the query runs. Measure a real bottleneck before optimizing. Reliability usually improves when markup exposes stable classes or data attributes for application behavior and when code handles both missing elements and delayed rendering.

10. Use selectors to capture a specific page element

The same selector idea is useful when you need a screenshot of one component instead of a whole page. Inspect the page, identify a stable CSS selector for the element, and pass it to a capture tool that supports selector-based element capture. A browser-based do-it-yourself workflow needs a browser runtime, page loading and wait logic, and output handling; selectors themselves only identify the target element.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from ScreenshotNeo. Its API can capture a URL in one GET request, including full-page or CSS-selector element captures. See the ScreenshotNeo API documentation.

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}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', bytes));

For selector-based capture, set the element selector option documented by the API to the selector you verified in the browser. Cookie banners, newsletter popups, and chat widgets are removed before the shot, and each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers identify the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000.

Sign up for ScreenshotNeo and get 1,000 free screenshots a month with no card.

11. FAQ

Can one selector match different element types?

Yes. Use a comma-separated selector list such as input, textarea. It matches either type.

Does an element’s own scoped query include that element?

No. container.querySelector() searches descendants of the container. To test the container itself, use container.matches(selector).

Should I use IDs or classes?

Use an ID when the target is a unique document landmark. Use classes or stable data attributes for reusable components and groups.

Can JavaScript select ::before or ::after?

No. Those pseudo-elements can be styled with CSS, but DOM selector methods return elements, not pseudo-elements.