CSS selectors are a good fit when a Selenium test needs to target a web element by stable attributes, hierarchy, or component state. A selector that is too broad can return the wrong node, while a selector tied to generated classes can break after a harmless frontend build, so the lookup should prove the exact element and value the test needs.
In Python, By.CSS_SELECTOR sends the selector to the browser and returns a WebElement from the current browsing context. Pairing it with WebDriverWait keeps the lookup from racing JavaScript-rendered content, and scoping child lookups from a parent element keeps repeated components from matching the wrong card, row, or form.
Prefer selectors built from stable id values, data-testid attributes, roles, names, or durable component attributes. CSS selectors search only the current page, frame, or shadow root, so switch into an iframe or shadow root before searching when the element is not in the top-level document.
from pathlib import Path from tempfile import TemporaryDirectory import os import shutil from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait HTML = """<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Selenium CSS selector demo</title> </head> <body> <main id="pricing"> <article class="plan-card" data-testid="pricing-plan" data-plan="starter" data-active="true"> <h2 class="plan-title">Starter</h2> <p class="plan-price">$9</p> </article> <article class="plan-card featured" data-testid="pricing-plan" data-plan="pro" data-active="true"> <h2 class="plan-title">Pro</h2> <p class="plan-price">$29</p> </article> <article class="plan-card" data-testid="pricing-plan" data-plan="archive" data-active="false" hidden> <h2 class="plan-title">Archive</h2> <p class="plan-price">$0</p> </article> </main> </body> </html> """ options = Options() options.add_argument("--headless=new") options.add_argument("--disable-dev-shm-usage") options.add_argument("--window-size=1280,720") if hasattr(os, "geteuid") and os.geteuid() == 0: options.add_argument("--no-sandbox") for browser_name in ("google-chrome", "chromium", "chromium-browser"): browser_path = shutil.which(browser_name) if browser_path: options.binary_location = browser_path break driver_path = shutil.which("chromedriver") service = Service(driver_path) if driver_path else Service() with TemporaryDirectory() as tmpdir: page = Path(tmpdir) / "pricing.html" page.write_text(HTML, encoding="utf-8") driver = webdriver.Chrome(service=service, options=options) try: driver.get(page.as_uri()) wait = WebDriverWait(driver, 10) pro_plan = wait.until( EC.visibility_of_element_located( (By.CSS_SELECTOR, "[data-testid='pricing-plan'][data-plan='pro']") ) ) price = pro_plan.find_element(By.CSS_SELECTOR, ".plan-price").text active_plans = driver.find_elements( By.CSS_SELECTOR, "[data-testid='pricing-plan'][data-active='true']" ) print(f"title: {driver.title}") print(f"selected_plan: {pro_plan.find_element(By.CSS_SELECTOR, '.plan-title').text}") print(f"selected_price: {price}") print(f"active_plan_count: {len(active_plans)}") finally: driver.quit()
The root-only --no-sandbox branch is for disposable containers that run Chrome as root. Omit that branch when local policy already runs the browser as an unprivileged user.
Related: How to configure ChromeDriver for Selenium
$ python3 selenium-element-find-css.py title: Selenium CSS selector demo selected_plan: Pro selected_price: $29 active_plan_count: 2
pro_plan = wait.until( EC.visibility_of_element_located( (By.CSS_SELECTOR, "[data-testid='pricing-plan'][data-plan='pro']") ) )
Prefer a stable id or data-* attribute when the application provides one. Generated class names and full DOM paths often change without changing the feature behavior.
price = pro_plan.find_element(By.CSS_SELECTOR, ".plan-price").text
The child selector runs from pro_plan, so .plan-price does not accidentally read a price from another card.
visible_errors = driver.find_elements(By.CSS_SELECTOR, ".field-error[role='alert']") if visible_errors: print(visible_errors[0].text)
find_element() raises an exception when nothing matches. find_elements() returns an empty list, which is easier to handle for optional messages, repeated rows, or validation errors.
pro_plan = wait.until( EC.visibility_of_element_located( (By.CSS_SELECTOR, "[data-testid='pricing-plan'][data-plan='pro']") ) )
A saved WebElement can become stale after JavaScript replaces the node. Keep the selector and wait for a fresh element before reading or clicking it again.
Related: How to troubleshoot stale element errors in Selenium
$ rm selenium-element-find-css.py