Comprehensive reference for css selectors reference covering essential concepts, practical examples, and production best practices.
Css Selectors Reference
This page is a practical reference for writing, debugging, and troubleshooting CSS selectors in engineering contexts: browser devtools, DOM scripting, test automation, and CSS-in-JS workflows. It matters whenever you need to target elements without adding IDs or modifying markup—whether you're building a one-off script, writing automated tests, or overriding styles in a compiled stylesheet. The following sections are organised by what you’re trying to accomplish, not by selector taxonomy, and include exact commands, flags, code snippets, and the error text you’ll actually see when things go wrong.
document.querySelector() and document.querySelectorAll()These are the most common entry points for CSS selectors in browser scripting. querySelector returns the first matching element or null; querySelectorAll returns a live NodeList (actually static in modern engines) of all matches.
// Select first element with class "alert"
const firstAlert = document.querySelector('.alert');
// Select all buttons inside a form
const allButtons = form.querySelectorAll('button');
// Match element that exists in the document
if (element.matches('.expanded')) { /* … */ }
Exact error text when selector is invalid:
SyntaxError: Failed to execute 'querySelector' on 'Document': '.class #id' is not a valid selector.
TypeError: Cannot read properties of null (reading 'addEventListener')
The first occurs if the selector string itself is malformed (unbalanced brackets, unescaped pseudo-class characters). The second means querySelector returned null and you tried to chain methods on it.
Sharp edges:
NodeList from querySelectorAll or null from querySelector. No error is thrown; your code proceeds and later fails when you assume an element exists.:hover, :focus, and other state-based selectors return null unless the element actually carries that state in the current DOM configuration.[ and ] or use quoted attribute values: [data-id="foo bar"] rather than [data-id=foo bar].Cypress wraps querySelectorAll internally. The primary command is cy.get(), which accepts a CSS selector string.
// First matching element
cy.get('.submit-button').click();
// All matches
cy.getAll('tr').each($row => { /* … */ });
// Failing assertion with exact message
await expect(cy.get('.missing')).to.exist
// => AssertionError: Timed out retrying after 4000ms: Expected dom element to exist, but never found.
Flags & timeouts: cy.get() accepts a second options object with timeout (default 16000ms). cy.contains('Submit') searches text content, not a pure CSS selector, but is often used alongside them.
Sharp edges in production:
data-cy attributes.:nth-child and :nth-of-type behave differently inside iframes and shadow DOMs. A selector like tr:nth-child(3) inside a table that itself is :nth-child(2) of a container may match the wrong row or none at all.cy.get() throws CypressError: Timed out retrying after … if the element never appears. This is often misread as a selector bug when the real issue is a race condition or network latency.Playwright uses page.locator(selector) which returns a Locator object. It is lazily evaluated and more resilient to stale element references.
const submit = page.locator('.submit-button');
await submit.click();
// Exact assertion failure text
await expect(submit).toBeVisible()
// => AssertionError: expectedLocator.toBeVisible() failed: expected element to be visible but it is not.
Flags & configuration: page.locator(selector, { timeout: 5000 }) sets per‑locator timeout. expect(locator).toHaveCount(1) verifies exact match count.
Sharp edges:
locator does not throw immediately if the selector matches nothing; it produces an empty locator. You must explicitly assert (toBeVisible(), toHaveText()) to surface the failure.$ or # that collide with Playwright’s internal locator syntax (e.g., page.locator('#my-id') is fine, but page.locator('$') will error). Always quote the selector string when in doubt.shadow prefix: page.locator('my-component shadow dom .inner'). Omitting shadow yields SelectorError: The selector "…" did not match any elements.Selenium’s find_element and find_elements map directly to CSS selector evaluation via the browser’s native engine.
WebElement button = driver.findElement(By.cssSelector(".submit-button"));
List<WebElement> rows = driver.findElements(By.cssSelector("tr.data-row"));
Exact error text:
org.openqa.selenium.NoSuchElementException: no such element: Unable to locate element: {"css selector": ".submit-button"}
Build info version: 4.15.0
In Python: selenium.common.exceptions.NoSuchElementException: Message: no such element: {"css selector": "…"}
Sharp edges:
find_element throws on zero matches; find_elements returns an empty list. Mixing the two causes StaleElementReferenceException if the DOM updates between calls.:not(), :nth-child, or attribute selectors with values containing spaces without explicit encoding.--headless), :hover and :focus selectors always evaluate to false. Tests that depend on hover states must use ActionChains or bypass the selector entirely.A selector’s power comes from combination, but order and spacing matter.
/* Valid: descendant */
div.form input[type="text"] { /* … */ }
/* Valid: direct child */
ul > li.active { /* … */ }
/* Valid: adjacent sibling */
h1 + p { /* … */ }
/* Common pitfall: spacing changes meaning */
div .active /* any .active inside div */
div.active /* element with both div and active classes */
Exact error when selector is malformed in a stylesheet:
CSSParserError: Unexpected identifier at line 3, column 12
Browsers will stop parsing the rule set entirely; subsequent rules are unaffected unless wrapped in @media or similar.
| Selector | What it actually matches | Common mismatch |
|---|---|---|
:nth-child(2n+1) | Odd‑indexed children *among all siblings* | Expecting odd‑indexed elements of a specific type |
:nth-of-type(2n+1) | Odd‑indexed children *of the same element type* | Using this when siblings are mixed tags |
:not(.active) | Every element except those with class active | Thinking it excludes descendants only, not all descendants |
:first-child | Element that is the first child of its parent | Expecting it to match the first .item in a list |
Concrete gotcha:
/* Matches the first <li> regardless of parent */
li:first-child { color: red; }
/* Does NOT match if the first child is a <span> */
ul > li:first-child { color: red; }
If your <ul> starts with <span>, the second rule matches nothing.
Specificity is calculated as (a, b, c, d) where a = inline styles, b = IDs, c = classes/attributes/pseudo-classes, d = element/tag names. A rule with specificity (0,1,0,0) beats (0,0,1,0).
Production breakage pattern: You add .btn { background: blue; } ( specificity 0,0,1,1 ) to override a button style. Later, a third‑party script injects <button id="action-btn"> and the existing rule #action-btn { … } (0,1,0,0) wins, but your .btn does not cascade down because the ID selector is more specific. The silent failure: button appears unstyled or inherits a default you didn’t intend.
Fix: Either increase your selector’s ID count (avoid if possible) or use !important sparingly, or restructure to target the shared parent and descend: .form .btn { … }.
$$ and Selector SearchChrome and Firefox expose a jQuery‑style $ and $$ in the DevTools console, but these are convenience wrappers around querySelector and querySelectorAll.
// First match
$('.alert-error');
// All matches
$$('script[type="application/json"]');
// Shorthand for complex selectors
$$('[data-tooltip]:not([data-state="disabled"])');
Exact console output when selector is invalid:
VM123:1 Uncaught SyntaxError: Failed to parse selector ".[data-state="
The console will not evaluate the selector and will print the error above, leaving the variable undefined.
When you type a selector into the Elements panel “Search” field (⌘+F / Ctrl+F), the browser highlights matches or reports:
No matching elements found
If the syntax is broken (e.g., unclosed bracket), the search field turns red and shows:
Invalid selector: "div[data-id
The most frequent production incident involving selectors is assuming querySelectorAll returned something because the selector “looks right.” A selector like #sidebar .nav-link returns [] if the markup changed from <div id="sidebar"> to `<
This page was rewritten on 10 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy