How to Find Elements by CSS Selectors in Selenium
Use Selenium’s CSS selector locator to find one or many elements, wait for dynamic content, and troubleshoot selectors that stop matching.

In Python Selenium, find one element with driver.find_element(By.CSS_SELECTOR, "#fname"). Find every match with driver.find_elements(By.CSS_SELECTOR, "table tbody tr"). For elements added or revealed asynchronously, combine the CSS selector with an explicit WebDriverWait condition instead of looking immediately.
CSS is one of WebDriver’s eight traditional locator strategies. Selenium describes the CSS strategy as locating elements that match a CSS selector. It works across Selenium language bindings, though each language spells the method and constant differently. This guide focuses on Python, then gives equivalent Java code and practical advice for choosing selectors, waiting, handling multiple matches, and diagnosing failures. [Selenium locator strategies](https://www.selenium.dev/documentation/webdriver/elements/locators/)
1. Set up Selenium and open a page
Install Selenium in the Python environment where the script will run:
python -m pip install selenium
Recent Selenium versions use Selenium Manager to help obtain a compatible browser driver when one is not already configured. Install a supported browser such as Chrome or Firefox, then use the corresponding WebDriver class. This minimal example opens a page and looks up an input by ID:
from selenium import webdriver
from selenium.webdriver.common.by import By
# Selenium Manager can manage the driver for supported local browsers.
driver = webdriver.Chrome()
try:
driver.get("https://www.selenium.dev/selenium/web/web-form.html")
name_input = driver.find_element(By.CSS_SELECTOR, "#my-text-id")
print(name_input.get_attribute("placeholder"))
finally:
driver.quit()
By.CSS_SELECTOR tells Selenium to interpret the second argument as a CSS selector. The selector is evaluated against the page’s current DOM. Selenium returns a WebElement for the first match or raises NoSuchElementException if none exists at lookup time.
2. Write CSS selectors for Selenium
Selenium passes a CSS selector to the browser; it does not use a special Selenium-only selector syntax. These common patterns cover most automation tasks:

| Target | Selector | Example |
|---|---|---|
| ID | #id |
#login |
| Class | .class-name |
.error-message |
| Tag and class | tag.class |
p.content |
| Attribute value | [attribute='value'] |
input[name='email'] |
| Descendant | ancestor descendant |
form#login input[name='email'] |
| Direct child | parent > child |
ul.menu > li |
| Several classes on one node | .class-a.class-b |
.card.featured |
| Position among siblings | :nth-child(n) |
table tbody tr:nth-child(2) |
Pass the selector as a Python string. Use double quotes around the Python string when the selector itself contains single quotes, as in "input[name='email']". A selector such as .card.featured matches a single element that has both classes; .card .featured instead matches a descendant with class featured inside an element with class card.
Prefer stable attributes
Selectors are only as reliable as the attributes and structure they depend on. Prefer a stable ID, name, or application-provided data attribute when available. For example, [data-testid='save'] can be clearer and less fragile than a long chain of layout classes if the application maintains that attribute as part of its testing contract.
Avoid depending on generated class names that change between builds, or long positional selectors that assume the page’s layout never changes. Use the shortest selector that identifies the intended element unambiguously. Validate it against the current DOM when the page changes or a lookup stops working.
3. Find one element or collect all matches
Use the singular method when the page should contain one matching element. Use the plural method when zero, one, or many matches are valid. The plural call returns a list; an empty list means no element matched at that moment.
from selenium.webdriver.common.by import By
first_name = driver.find_element(By.CSS_SELECTOR, "#fname")
content_paragraph = driver.find_element(By.CSS_SELECTOR, "p.content")
rows = driver.find_elements(By.CSS_SELECTOR, "table tbody tr")
for row in rows:
print(row.text)
# A plural lookup is useful when no match is a normal outcome.
notices = driver.find_elements(By.CSS_SELECTOR, ".notice")
if not notices:
print("No notices are displayed")
The singular method returns the first match in document order, not a guarantee that only one match exists. If uniqueness matters, explicitly check the plural result:
matches = driver.find_elements(By.CSS_SELECTOR, "button.save")
if len(matches) != 1:
raise RuntimeError(f"Expected one save button, found {len(matches)}")
save_button = matches[0]
4. Wait for elements on dynamic pages
JavaScript applications often insert elements, reveal them, or enable controls after navigation. An immediate lookup only checks the DOM at that instant. Use an explicit wait to poll for the state the next action actually requires.

Import WebDriverWait and expected conditions, then wait for clickability before interacting:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
try:
driver.get("https://example.com")
wait = WebDriverWait(driver, 10)
submit = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "button.submit"))
)
submit.click()
finally:
driver.quit()
Remove the accidental leading space before driver = if copying this snippet exactly; Python code at the top level must not be indented. The corrected setup line is driver = webdriver.Chrome(). The condition receives a locator tuple: (By.CSS_SELECTOR, "button.submit").
Choose the wait condition based on what must be true:
| Condition | What it establishes | Use it when |
|---|---|---|
presence_of_element_located |
A matching node exists in the DOM. | You need to inspect an attribute or DOM-backed state; visibility is not required. |
visibility_of_element_located |
A matching node exists and is displayed. | You need to read visible content or interact after it appears. |
presence_of_all_elements_located |
At least one matching node exists; returns the current matches. | You need a dynamically populated collection to appear. |
element_to_be_clickable |
The element is visible and enabled. | You are about to click or otherwise interact with an enabled control. |
Example: wait until a results list appears, then collect its rows:
wait = WebDriverWait(driver, 10)
rows = wait.until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, "table.results tbody tr")
)
)
for row in rows:
print(row.text)
Presence does not mean the element is visible, and visibility does not mean it is enabled. Clickability combines visibility and enabled state. Waiting for the narrowest useful condition makes failures easier to interpret than adding a fixed sleep, which may be too short on a slow run and unnecessarily long on a fast one.
5. Complete example: wait, read, and handle missing results
This script opens a page, waits for a form field, fills it, submits, and waits for the result. Replace the URL and selectors with those from the application under test:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
url = "https://example.com/search"
driver = webdriver.Chrome()
wait = WebDriverWait(driver, 12)
try:
driver.get(url)
query = wait.until(
EC.visibility_of_element_located(
(By.CSS_SELECTOR, "input[name='q']")
)
)
query.send_keys("selenium")
submit = wait.until(
EC.element_to_be_clickable(
(By.CSS_SELECTOR, "button[type='submit']")
)
)
submit.click()
results = wait.until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, "ul.results > li")
)
)
for result in results:
print(result.text)
except TimeoutException:
print("The expected element or results did not appear before the wait expired")
except NoSuchElementException:
print("A direct lookup found no matching element")
finally:
driver.quit()
The exception handling is optional, but it makes a failed run easier to diagnose. In a test suite, you may prefer to let the exception fail the test so the failure is visible to the test runner.
6. Java equivalent
The same locator strategy is available in Java through By.cssSelector. The singular method returns one element; the plural method returns a list:
import java.time.Duration;
import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
WebDriver driver = new ChromeDriver();
try {
driver.get("https://example.com");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement button = wait.until(
ExpectedConditions.elementToBeClickable(By.cssSelector("button.submit"))
);
button.click();
List<WebElement> rows = driver.findElements(
By.cssSelector("table tbody tr")
);
for (WebElement row : rows) {
System.out.println(row.getText());
}
} finally {
driver.quit();
}
Other Selenium bindings use their own naming conventions, but the selector remains ordinary CSS. Check the binding’s API documentation for its exact method names.
7. CSS selectors compared with other locator strategies
CSS is concise for IDs, classes, attributes, and relationships in the DOM. ID and class-name locators can be convenient for a single simple attribute. XPath can express some relationships CSS cannot, including matching an element by its text content or navigating to certain ancestors. The best locator is the one that is readable, stable, and tied to the page’s intended contract.
| Strategy | Useful for | Trade-off |
|---|---|---|
| CSS selector | IDs, classes, attributes, descendants, children, and structural patterns. | Does not select by rendered text content alone. |
| ID | A unique, stable ID. | Only works when the relevant ID is known and stable. |
| Class name | A single class that identifies the target. | Cannot express combinations and relationships as flexibly as CSS. |
| XPath | Text-based queries and relationship navigation beyond CSS’s selector model. | Can become harder to read when overused. |
8. Troubleshooting CSS locator failures
| Symptom | Likely cause | What to do |
|---|---|---|
NoSuchElementException |
The selector does not match the current DOM, or the lookup happened before insertion. | Inspect the live DOM, test the selector, and use an explicit wait if content is delayed. |
TimeoutException from a wait |
The matching state never became true within the timeout. | Confirm the selector and page state, then check whether the element is hidden, disabled, or in another browsing context. |
| Element found but not interactable | The node exists but is hidden, covered, disabled, or not yet ready. | Wait for visibility or clickability as appropriate; check overlays and enabled state. |
| More elements than expected | The selector is broad or matches repeated components. | Scope it beneath a stable container and inspect the plural result before acting. |
| Works locally but fails in CI | Timing, browser size, environment, or browser version changes what is rendered or when. | Use state-based waits, control the test environment, and verify the rendered DOM in the failing environment. |
| Element appears absent despite a correct selector | It is inside an iframe or a shadow root. | Switch into the iframe first; for shadow DOM, access the relevant shadow root using the support available in your Selenium version. |
Debug in a deliberate order
- Inspect the current DOM and confirm the selector matches the intended node.
- Check whether the page has finished the action that inserts or reveals the element.
- Use an explicit wait for presence, visibility, or clickability according to the next step.
- Check iframe and shadow-root boundaries; a selector search is scoped to the current context.
- Use
find_elementswhen multiple matches or zero matches are expected, and handle the returned collection intentionally. - Replace generated classes or brittle positional paths with a stable ID, name, data attribute, or semantic relationship if the application exposes one.
9. Performance, reliability, and cost notes
A CSS lookup is a local browser query, but reliability depends more on selector quality and page state than on making the selector shorter. A stable, scoped selector is easier to maintain than a long chain tied to incidental layout. For collections, locate the shared container or the repeated items once and avoid repeatedly querying the same broad page region inside a loop.
Use explicit waits for state transitions and keep timeouts proportionate to the application’s expected behavior. A timeout is a maximum wait, not a delay imposed on every successful run. Avoid mixing implicit and explicit waits without understanding their interaction; it can make elapsed wait time less predictable. Always close the browser with quit(), including on exceptions, so the session and browser process do not linger.
Local Selenium’s direct costs depend on the browser, machine, and any hosted browser infrastructure you choose; the locator API itself does not define a per-selector fee. For a task whose output is a screenshot rather than an interactive test, a managed screenshot endpoint can avoid browser setup and maintenance.
10. Or skip the browser setup
If your task is to capture a page image rather than interact with its controls, ScreenshotNeo returns a PNG, JPEG, WebP, or PDF from one GET request. See the ScreenshotNeo API documentation for options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write("shot.webp", res);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, and cache hits are never billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents use screenshot tools. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.
Sign up for ScreenshotNeo and get 1,000 free screenshots a month with no card.
11. FAQ
Does find_element return every match?
No. It returns the first matching element. Use find_elements to get a list of all current matches.
Can a CSS selector match text such as “Continue”?
CSS selectors do not provide a general text-content selector. Use another strategy, such as XPath, when the visible text is the locator requirement.
Should I add a fixed sleep before every lookup?
No. A fixed sleep delays every run and may still be too short. Wait for the specific DOM, visibility, or interaction state your next step needs.
Can I use CSS selectors inside an iframe automatically?
No. First switch WebDriver to the iframe context, then locate elements within it. Switch back to the top-level document when finished.