CSS Selector Cheat Sheet for Playwright and Selenium
Every CSS selector an automation tester actually uses: the quick reference table of Playwright-only pseudo-classes first, then the full standard CSS arsenal, table-locating strategies, and copy-paste examples for Playwright and Selenium.
1. Quick reference: Playwright pseudo-classes for tables
These are the selectors that make CSS usable on real-world tables. Most are Playwright-only extensions (they will not work in document.querySelector or Selenium); the standard ones are marked.
Pseudo-class
Scope
Use case in tables
Standard CSS?
:has(selector)
Descendant match, returns the parent
Row containing a specific cell or button: tr:has(button.delete)
Yes (modern browsers)
:has-text("str")
Own + children's text, substring, case-insensitive
Quick row or cell text search: tr:has-text("Pramod")
No, Playwright only
:text("str")
Smallest element with own text, substring
Cell text match without knowing the tag depth: td :text("Delivered")
Nth row or column overall: tbody tr:nth-child(3), td:nth-child(2)
Yes
:nth-match(sel, n)
Nth of the whole matched set
Nth matching row, not Nth overall: :nth-match(tr:has-text("Fail"), 2)
No, Playwright only
:not(selector)
Exclusion
Skip header, footer, or total rows: tr:not(.header):not(:has(th))
Yes
:visible
Rendering state
Skip hidden or collapsed rows: tr:visible
No, Playwright only
The one distinction people miss::nth-child(3) counts all siblings, while :nth-match(sel, 3) counts only the elements your selector matched. "Third row that contains Fail" is :nth-match(tr:has-text("Fail"), 3), never tr:has-text("Fail"):nth-child(3).
2. The basics
Selector
Matches
Example
tag
Every element of that tag
button, input, tr
#id
The element with that id
#login-btn
.class
Elements with that class
.error-message
tag.class
Tag with class (no space!)
button.primary
.a.b
Element with BOTH classes
.btn.disabled
sel1, sel2
Either selector (union)
input, textarea
*
Any element
div > *
3. Attribute selectors
The workhorses for dynamic apps: match on data-testid, name, placeholder, partial ids.
Selector
Meaning
Example
[attr]
Attribute exists
[disabled], [data-testid]
[attr="v"]
Exactly equals
[data-testid="submit"]
[attr^="v"]
Starts with
[id^="user_"] for generated ids
[attr$="v"]
Ends with
[src$=".png"]
[attr*="v"]
Contains substring
[class*="error"]
[attr~="v"]
Contains the whole word
[class~="active"] (space-separated list)
[attr|="v"]
Equals or starts with v-
[lang|="en"] matches en, en-US
[attr="v" i]
Case-insensitive match
[placeholder="email" i]
Dynamic id survival kit. Framework-generated ids like input-4821-field change every build. Anchor on the stable part: [id^="input-"][id$="-field"], or better, ask the devs for a data-testid and use [data-testid="email"].
4. Combinators
Combinator
Relationship
Example
A B (space)
B anywhere inside A (any depth)
form input
A > B
B is a direct child of A
ul > li (skips nested lists)
A + B
B immediately after A (same parent)
label + input the input right after its label
A ~ B
Any B after A (same parent)
h2 ~ p every paragraph after the heading
Standard CSS has no parent or ancestor combinator: you can walk down, never up. :has() is the exception that fixes it: div:has(> img) means "div whose direct child is an img", which effectively selects a parent by its child.
5. Structural pseudo-classes
Selector
Matches
Table example
:first-child / :last-child
First / last among siblings
tbody tr:first-child, td:last-child (last column)
:nth-child(odd) / (even)
Alternating siblings
tr:nth-child(odd) zebra rows
:nth-child(an+b)
Formula positions
tr:nth-child(3n+1) every third row starting at 1
:nth-last-child(n)
Nth from the end
tr:nth-last-child(2) second-to-last row
:nth-of-type(n)
Nth of that tag only
p:nth-of-type(2) ignores non-p siblings
:only-child
The only sibling
td:only-child a one-cell row (often a "no data" row)
:empty
No children, no text
td:empty blank cells
6. State pseudo-classes
Selector
Matches
Automation use
:checked
Checked checkbox/radio/option
Assert a row's checkbox is on: tr input:checked
:disabled / :enabled
Form control state
Wait for button[type=submit]:enabled
:focus
The focused element
Keyboard-navigation checks
:required / :optional
Form validation attributes
Count required fields on a form
:placeholder-shown
Input still showing placeholder
Detect untouched fields
7. Locating in tables, end to end
The scenario every interview asks: act on a cell based on another cell in the same row. Given this table:
Order
Customer
Status
Action
#1001
Pramod
Delivered
#1002
Anita
Pending
#1003
Rahul
Delivered
// The row that contains "Anita" (standard :has + Playwright :has-text)
tr:has(td:has-text("Anita"))
// Anita's STATUS cell (3rd column of that row)
tr:has(td:has-text("Anita")) td:nth-child(3)
// Click Delete on Anita's row
tr:has-text("Anita") button
// Second DELIVERED row (matched set, not DOM position)
:nth-match(tr:has-text("Delivered"), 2)
// Every data row except the header, only if visible
tr:not(:has(th)):visible
// Exact status only: "Delivered", never "Not Delivered"
td:text-is("Delivered")
8. Using them in Playwright and Selenium
// Playwright (TypeScript) - css= prefix is optionalawait page.locator('tr:has-text("Anita") button').click();
await expect(page.locator('tr:has(td:has-text("Anita")) td:nth-child(3)'))
.toHaveText('Pending');
# Selenium (Python) - STANDARD CSS ONLY: no :has-text, :text, :nth-match, :visible
driver.find_element(By.CSS_SELECTOR, 'tr:has(td.customer) button')
# For text-based matching in Selenium, switch to XPath:
driver.find_element(By.XPATH, '//tr[td[text()="Anita"]]//button')
Portability warning.:has-text, :text, :text-is, :text-matches, :nth-match, :visible exist only inside Playwright's selector engine. In Selenium, DevTools, or querySelector they throw. When you need text matching outside Playwright, use XPath: our XPath Master Cheat Sheet covers every function and axis.
9. Pitfalls that cost real debugging hours
Space vs no space.button.primary (button WITH class) vs button .primary (element with class INSIDE a button). One character, opposite meaning.
:nth-child counts everything. A <style> or comment-free text node does not count, but ANY element sibling does. If a hidden <tr class="spacer"> sits in the table, your column math shifts.
Classes are not words.[class*="act"] also matches "inactive". Prefer [class~="active"] or .active.
No walking up. If you find yourself needing "the parent of X", the answer is :has(), or XPath's parent:: axis.
Auto-generated class soup..css-1x2y3z changes every deploy. Anchor on data-testid, roles, or stable attributes, and push the team to add them.