DevToys Web Pro iconDevToys Web ProBlog
Ohodnoťte nás:
Vyzkoušejte rozšíření pro prohlížeč:
← Back to Blog

CSS Selector Tester Guide: Specificity, Combinators & nth-child

10 min read

CSS selectors power both styling and DOM querying. Whether you are writing a scraper with document.querySelectorAll, debugging a specificity conflict in a stylesheet, or mastering the subtleties of :nth-child, understanding how the browser evaluates selectors will save you from hours of trial and error. Use the CSS Selector Tester to run every example in this guide against live HTML instantly.

This guide covers the four areas that trip up developers most: the specificity calculation algorithm, CSS combinators, the :nth-child vs :nth-of-type mismatch, and attribute selectors — plus a comparison with XPath for scraping tasks.

Specificity: The (a, b, c) Tuple

When two selectors match the same element and set the same property, the browser applies the one with higher specificity. Specificity is calculated as a three-component tuple (a, b, c):

  • a — ID selectors. Each #id in the selector adds 1 to a.
  • b — Class, attribute, and pseudo-class selectors. Each .class, [attr], or :hover adds 1 to b.
  • c — Type selectors and pseudo-elements. Each element name like div or ::before adds 1 to c.

The universal selector *, combinators (>, +, ~, space), and the :where() pseudo-class contribute zero to specificity. Inline styles have specificity (1, 0, 0, 0) — treated as a fourth component that beats any selector. !important overrides everything and should be avoided in production code.

SelectorabcTotal
*000(0,0,0)
p001(0,0,1)
p.note011(0,1,1)
ul li.active012(0,1,2)
#nav a101(1,0,1)
#nav .item:hover120(1,2,0)

Tuples are compared left to right: a selector with a=1 always wins over one with a=0, regardless of how large b and c are. There is no integer overflow — 11 classes do not beat one ID.

/* (0,0,1) — lowest */
p { color: black; }

/* (0,1,1) — wins over the above */
p.note { color: navy; }

/* (1,0,1) — wins over both */
#content p { color: darkgreen; }

/* Inline style — wins over all selectors */
/* <p style="color: red"> */

Combinators: Four Ways to Traverse the DOM

Combinators describe the structural relationship between two simple selectors. There are four of them, and choosing the right one tightens your selector and avoids unintended matches.

  • Descendant (space). article p matches every <p> that is a descendant of <article> at any depth. Convenient but broad — it matches deeply nested paragraphs you may not intend to select.
  • Child (>). ul > li matches only <li> elements that are direct children of a <ul>. Nested lists are excluded.
  • Adjacent sibling (+). h2 + p matches the first <p> immediately following an <h2> at the same level. Useful for styling the lead paragraph after a heading.
  • General sibling (~). h2 ~ p matches all <p> siblings that come after an <h2>, not just the first one.
/* Descendant: any <a> inside .nav */
.nav a { text-decoration: none; }

/* Child: only direct <li> children */
.menu > li { display: inline-block; }

/* Adjacent sibling: <p> immediately after <h2> */
h2 + p { font-size: 1.1em; font-weight: 500; }

/* General sibling: all <p> after a <details> */
details ~ p { margin-top: 0.5rem; }

A common mistake is using the descendant combinator when the child combinator is intended. Consider a nested navigation menu: .nav a styles links at every depth, while .nav > li > a styles only the top-level links, leaving sub-menu links unstyled.

:nth-child() vs :nth-of-type() — The Classic Mismatch

Both pseudo-classes accept the An+B formula (e.g., :nth-child(2n+1) for odd elements), but they count differently and the difference catches developers off guard when element types are mixed.

:nth-child(n) counts all siblings regardless of tag name, then checks whether the element at that position also matches the rest of the selector. :nth-of-type(n) counts only siblings of the same tag name.

<section>
  <h2>Title</h2>      <!-- child 1 -->
  <p>First para.</p>  <!-- child 2, p:nth-of-type(1) -->
  <p>Second para.</p> <!-- child 3, p:nth-of-type(2) -->
  <p>Third para.</p>  <!-- child 4, p:nth-of-type(3) -->
</section>
/* Matches child #2 overall — the first <p> — because
   child 1 is the <h2>, not a <p>. Works as expected here. */
p:nth-child(2) { color: steelblue; }

/* Matches the first <p> counted among <p> siblings only.
   Same result here, but diverges when the <h2> is removed. */
p:nth-of-type(1) { color: steelblue; }

/* :nth-child(1) would match the <h2>, NOT a <p>,
   so "p:nth-child(1)" matches nothing in this markup. */
p:nth-child(1) { color: red; } /* no match */

/* Highlight every other <p> starting from the first */
p:nth-of-type(odd) { background: #f5f5f5; }

The rule of thumb: use :nth-of-type when the parent contains a mix of element types (headings, paragraphs, figures) and you only want to count one type. Use :nth-child when all siblings are the same tag (a pure list of <li> or <td> elements) to avoid surprises.

Attribute Selectors

Attribute selectors let you match elements by the presence or value of an HTML attribute without adding extra classes. They are invaluable in scrapers and in styling third-party markup you cannot modify.

SyntaxMatchesExample
[attr]Element has the attribute (any value)[data-tooltip]
[attr="val"]Exact value match[type="submit"]
[attr^="val"]Value starts with val[href^="https"]
[attr$="val"]Value ends with val[href$=".pdf"]
[attr*="val"]Value contains val anywhere[class*="btn-"]
[attr~="val"]Whitespace-separated list contains val[class~="active"]
[attr|="val"]Equals val or starts with val-[lang|="en"]
[attr="val" i]Case-insensitive match[type="TEXT" i]
/* Style all external links */
a[href^="http"]:not([href*="yourdomain.com"]) {
  padding-right: 1em;
  background: url('/icons/external.svg') no-repeat right center;
}

/* Highlight required fields */
input[required] {
  border-left: 3px solid tomato;
}

/* Target data attributes set by JavaScript */
[data-state="loading"] .spinner { display: block; }
[data-state="ready"]   .spinner { display: none; }

/* Match PDF download links */
a[href$=".pdf"]::after {
  content: " (PDF)";
  font-size: 0.8em;
  color: gray;
}

Note that [class*="btn-"] is not equivalent to the class selector .btn-primary. The attribute selector performs a raw substring match on the entire attribute string, so it will also match class="my-btn-primary". For precise class matching without JavaScript, prefer [class~="btn-primary"] or the plain class selector.

Testing Selectors with querySelectorAll

The DOM API exposes CSS selector matching through two methods: document.querySelector(selector) returns the first match, and document.querySelectorAll(selector) returns a static NodeList of all matches. Both are available in every modern browser and in Node.js environments with a DOM library.

// Select all external links and log their hrefs
const externalLinks = document.querySelectorAll('a[href^="http"]');
externalLinks.forEach(link => console.log(link.href));

// Find the first submit button in a form
const submitBtn = document.querySelector('form button[type="submit"]');
console.log(submitBtn?.textContent);

// Count table rows (excluding header)
const rows = document.querySelectorAll('table tbody tr');
console.log(`${rows.length} data rows`);

// Combine attribute and pseudo-class selectors
const checkedBoxes = document.querySelectorAll('input[type="checkbox"]:checked');
const values = [...checkedBoxes].map(el => el.value);

// Scope to a subtree — pass element as context to closest parent
const nav = document.querySelector('#main-nav');
const navLinks = nav?.querySelectorAll('a[href]') ?? [];

In the browser DevTools console you can also use $$(selector) as a shorthand for document.querySelectorAll — it returns a real Array rather than a NodeList, so array methods like .map() and .filter() work directly without spreading. This shorthand is only available in the DevTools console, not in page scripts.

CSS Selectors vs XPath for Web Scraping

Both CSS selectors and XPath can query HTML, and many scrapers support both. Choosing the right tool depends on the task:

CapabilityCSS SelectorsXPath
Browser native APIquerySelectorAll — built indocument.evaluate — built in but verbose
Syntax brevityConcise for common patternsVerbose but expressive
Select by text contentNo (CSS4 :has() partial workaround)Yes — [text()='Buy']
Traverse upward (parent/ancestor)No (CSS has no parent selector)*Yes — ancestor:: axis
Attribute extractionVia JS .getAttribute()Direct — @href returns value
Readability for simple queriesHigher — familiar to CSS authorsLower — unfamiliar syntax
Scraper library supportExcellent (Cheerio, Playwright, Puppeteer)Good (lxml, Scrapy, Selenium)

* CSS Level 4 adds :has() which allows limited upward selection (e.g., li:has(> a.active)), but it is not the same as a full ancestor axis.

Use CSS selectors when: you are working in a JavaScript environment, the selector library you use (Cheerio, Playwright) is CSS-first, or the queries are simple element/class/attribute matches.

Use XPath when: you need to match by text content ([text()=]), traverse upward to a parent or ancestor, or work in a Python/lxml pipeline where XPath is the natural choice. See the XPath Tester for side-by-side experimentation with XPath expressions.

For pattern-based string matching within selector predicates or extracted text, the Regex Cheatsheet is a useful companion reference.

Best Practices

  • Prefer class selectors over complex combinators. A single class like .card-title is more robust and readable than section > div:first-child > h3, which breaks on any DOM restructure.
  • Use data attributes for JavaScript hooks. Write [data-action="submit"] rather than coupling your JS to style classes like .btn-primary. Styling and behaviour selectors should be independent.
  • Scope querySelectorAll to a subtree. Call container.querySelectorAll(...) rather than document.querySelectorAll(...) to limit the search to the relevant DOM subtree — faster and less likely to match unrelated elements.
  • Avoid over-qualified selectors. div.container ul li a.link repeats information the browser already knows and makes the selector harder to maintain. a.link is usually sufficient.
  • Test incrementally. Build your selector one component at a time in the CSS Selector Tester to verify each step before adding the next combinator or pseudo-class.

Ready to put these patterns into practice? Open the CSS Selector Tester and paste in any HTML document to run every selector from this guide against your own markup in seconds — no browser DevTools required.