The Testing Academy · Playwright Practice

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.

Quick reference table Playwright pseudo-classes Attribute operators Table strategies
On this page
  1. Quick reference: Playwright pseudo-classes
  2. The basics
  3. Attribute selectors
  4. Combinators
  5. Structural pseudo-classes
  6. State pseudo-classes
  7. Locating in tables
  8. Playwright and Selenium usage
  9. Pitfalls

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-classScopeUse case in tablesStandard CSS?
:has(selector)Descendant match, returns the parentRow containing a specific cell or button: tr:has(button.delete)Yes (modern browsers)
:has-text("str")Own + children's text, substring, case-insensitiveQuick row or cell text search: tr:has-text("Pramod")No, Playwright only
:text("str")Smallest element with own text, substringCell text match without knowing the tag depth: td :text("Delivered")No, Playwright only
:text-is("str")Own text, exact matchAvoid partial-match false positives: :text-is("Total") skips "Subtotal"No, Playwright only
:text-matches("re","i")Own text, regular expressionPattern-shaped cell values (dates, scores): td:text-matches("\\d+%")No, Playwright only
:nth-child(n)DOM position among siblingsNth row or column overall: tbody tr:nth-child(3), td:nth-child(2)Yes
:nth-match(sel, n)Nth of the whole matched setNth matching row, not Nth overall: :nth-match(tr:has-text("Fail"), 2)No, Playwright only
:not(selector)ExclusionSkip header, footer, or total rows: tr:not(.header):not(:has(th))Yes
:visibleRendering stateSkip hidden or collapsed rows: tr:visibleNo, 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

SelectorMatchesExample
tagEvery element of that tagbutton, input, tr
#idThe element with that id#login-btn
.classElements with that class.error-message
tag.classTag with class (no space!)button.primary
.a.bElement with BOTH classes.btn.disabled
sel1, sel2Either selector (union)input, textarea
*Any elementdiv > *

3. Attribute selectors

The workhorses for dynamic apps: match on data-testid, name, placeholder, partial ids.

SelectorMeaningExample
[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

CombinatorRelationshipExample
A B (space)B anywhere inside A (any depth)form input
A > BB is a direct child of Aul > li (skips nested lists)
A + BB immediately after A (same parent)label + input the input right after its label
A ~ BAny 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

SelectorMatchesTable example
:first-child / :last-childFirst / last among siblingstbody tr:first-child, td:last-child (last column)
:nth-child(odd) / (even)Alternating siblingstr:nth-child(odd) zebra rows
:nth-child(an+b)Formula positionstr:nth-child(3n+1) every third row starting at 1
:nth-last-child(n)Nth from the endtr:nth-last-child(2) second-to-last row
:nth-of-type(n)Nth of that tag onlyp:nth-of-type(2) ignores non-p siblings
:only-childThe only siblingtd:only-child a one-cell row (often a "no data" row)
:emptyNo children, no texttd:empty blank cells

6. State pseudo-classes

SelectorMatchesAutomation use
:checkedChecked checkbox/radio/optionAssert a row's checkbox is on: tr input:checked
:disabled / :enabledForm control stateWait for button[type=submit]:enabled
:focusThe focused elementKeyboard-navigation checks
:required / :optionalForm validation attributesCount required fields on a form
:placeholder-shownInput still showing placeholderDetect 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:

OrderCustomerStatusAction
#1001PramodDelivered
#1002AnitaPending
#1003RahulDelivered
// 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 optional
await 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

Practice all of these against real markup: the tables practice page and the rest of the Playwright practice library are built for exactly this. Struggling to find a stable locator at work? SnapLocator generates and heals them.

Next: the XPath Master Cheat Sheet →