CSS selectors are the workhorse of professional Selenium automation. They strike the ideal balance between power and readability — flexible enough to target almost any element, fast because they use the browser’s native CSS engine, and concise enough to keep test code readable. When an element lacks an ID or name, a well-crafted CSS selector is your next best option. This lesson teaches you the selector patterns you will use daily.
CSS Selector Syntax — From Simple to Advanced
CSS selectors range from trivially simple (selecting by tag or class) to surgically precise (combining attributes, hierarchy, and pseudo-classes). Learning the patterns in order of complexity lets you reach for the simplest one that works.
from selenium.webdriver.common.by import By
# ── CSS Selector Patterns — from simple to advanced ──
CSS_PATTERNS = [
# --- Basic selectors ---
{
"name": "Tag",
"selector": "input",
"matches": "All <input> elements",
"use": "Rarely alone — too broad",
},
{
"name": "Class",
"selector": ".btn-primary",
"matches": "Elements with class='btn-primary'",
"use": "When class is semantically unique",
},
{
"name": "ID",
"selector": "#login-button",
"matches": "Element with id='login-button'",
"use": "Fastest — same as By.ID but in CSS syntax",
},
# --- Attribute selectors ---
{
"name": "Exact attribute",
"selector": "input[type='email']",
"matches": "<input type='email'>",
"use": "Form fields by type, data-testid, name, etc.",
},
{
"name": "Attribute starts with",
"selector": "input[id^='user']",
"matches": "Input where id starts with 'user' (user-name, user-email)",
"use": "Elements with predictable ID prefixes",
},
{
"name": "Attribute ends with",
"selector": "input[id$='name']",
"matches": "Input where id ends with 'name'",
"use": "Elements with predictable ID suffixes",
},
{
"name": "Attribute contains",
"selector": "input[id*='login']",
"matches": "Input where id contains 'login' anywhere",
"use": "Partial match when exact value varies",
},
{
"name": "data-testid",
"selector": "[data-testid='submit-order']",
"matches": "Any element with data-testid='submit-order'",
"use": "Purpose-built test attributes — most stable option",
},
# --- Hierarchy selectors ---
{
"name": "Descendant",
"selector": "form.login input",
"matches": "Any <input> inside <form class='login'> at any depth",
"use": "Scoping elements within a container",
},
{
"name": "Direct child",
"selector": "ul.menu > li",
"matches": "Only direct <li> children of <ul class='menu'>",
"use": "Avoid matching nested list items",
},
{
"name": "Adjacent sibling",
"selector": "label + input",
"matches": "<input> immediately after a <label>",
"use": "Targeting an input relative to its label",
},
# --- Positional pseudo-classes ---
{
"name": "First child",
"selector": "ul.products > li:first-child",
"matches": "First <li> in the product list",
"use": "Selecting the first item in a list",
},
{
"name": "Last child",
"selector": "ul.products > li:last-child",
"matches": "Last <li> in the product list",
"use": "Selecting the last item in a list",
},
{
"name": "Nth child",
"selector": "table tbody tr:nth-child(3)",
"matches": "Third row in the table body",
"use": "Selecting a specific row or item by position",
},
# --- Combining selectors ---
{
"name": "Multiple classes",
"selector": ".btn.btn-primary.disabled",
"matches": "Element with ALL three classes",
"use": "When a single class is ambiguous",
},
{
"name": "Complex combination",
"selector": "div.checkout-form input[type='text']:not([disabled])",
"matches": "Enabled text inputs inside checkout form",
"use": "Precise targeting with exclusion",
},
]
print("CSS Selector Patterns for Selenium")
print("=" * 70)
for p in CSS_PATTERNS:
print(f"\n {p['name']}")
print(f" Selector: {p['selector']}")
print(f" Matches: {p['matches']}")
print(f" Use: {p['use']}")
[data-testid='value'], [name='email'], [type='submit']) are the most underused yet most stable CSS selectors. Unlike classes (which change for styling reasons) or positions (which change when elements are reordered), attributes like data-testid, name, and type are structural — they describe what the element is, not how it looks. Favour attribute selectors over class-based ones whenever possible.document.querySelectorAll("your-selector"). It returns a list of matching elements instantly. If it returns 0, your selector does not match. If it returns more than 1, your selector is too broad. This 10-second check saves minutes of debugging failed tests.div.main > div.content > div.section > div.row > div.col > form > div > input. These selectors encode the exact DOM structure and break whenever a developer adds, removes, or restructures a wrapper div. Instead, jump directly to the target: form.checkout input[name='email']. Shorter selectors are more readable, faster to execute, and far more resilient to DOM changes.Common Mistakes
Mistake 1 — Using purely positional selectors
❌ Wrong: div:nth-child(3) > span:nth-child(2) — breaks when any element is added or reordered.
✅ Correct: [data-testid='product-price'] or .product-card .price — targets by meaning, not position.
Mistake 2 — Not scoping selectors within a container
❌ Wrong: input[type='text'] — matches every text input on the entire page, potentially returning the wrong one.
✅ Correct: form.shipping input[type='text'] — scoped to the shipping form, eliminating ambiguity.