← Back

Playwright CSS Locators

Master CSS selectors in Playwright with real-time automation examples, robust locator strategies, and a practical comparison with Selenium.

1. What Are CSS Locators?

A CSS locator identifies HTML elements using CSS selector syntax. Playwright can use CSS selectors directly through page.locator().

HTML DOM
→
CSS Selector
→
Playwright Locator
→
Action / Assertion

Simple

Select elements using tags, IDs, classes and attributes.

Powerful

Combine selectors to target specific elements in complex DOM structures.

Composable

Build locators by chaining and filtering instead of creating huge XPath expressions.

2. Basic CSS Locator Syntax
page.locator("input") Select all input elements.
page.locator("#username") Select an element by ID.
page.locator(".login-button") Select elements by class.
page.locator('input[name="email"]') Select an input by its name attribute.
page.locator('button[type="submit"]') Select a submit button.
Basic CSS selectors - Python
from playwright.sync_api import Page

def test_basic_css_locators(page: Page):

    page.goto("https://example.test/login")

    page.locator("#username").fill("john@example.com")

    page.locator("#password").fill("secret123")

    page.locator('button[type="submit"]').click()
3. CSS ID, Class and Attribute Selectors
Selector Example Meaning
ID #email Element with id="email"
Class .primary-button Element containing the class
Attribute input[name="email"] Input with a matching name
Multiple attributes input[type="text"][name="email"] Both attributes must match
Descendant form input Input inside a form
Child form > button Direct child button
4. Real-Time Login Example

Imagine an enterprise login page:

Sample HTML
<form id="login-form">

    <input
        id="email"
        name="email"
        type="email"
        class="form-control"
    />

    <input
        id="password"
        name="password"
        type="password"
        class="form-control"
    />

    <button
        type="submit"
        class="login-button"
    >
        Sign In
    </button>

</form>
Playwright test
from playwright.sync_api import expect

def test_login(page):

    page.goto("https://example.test/login")

    page.locator("#email").fill("john@example.com")

    page.locator("#password").fill("Password123")

    page.locator('button[type="submit"]').click()

    expect(page).to_have_url(r".*/dashboard")

    expect(
        page.locator(".dashboard-header")
    ).to_be_visible()
Professional idea: Use CSS when it gives you a stable, readable selector. If the application provides reliable semantic attributes such as accessible roles or test IDs, those may be preferable to deeply coupled CSS.
5. CSS Attribute Selectors

Modern applications often have useful attributes that make CSS selectors much more precise.

Attribute selectors
# Exact attribute
page.locator('[data-testid="login-button"]')

# Attribute starts with
page.locator('[data-testid^="user-"]')

# Attribute ends with
page.locator('[data-testid$="-button"]')

# Attribute contains
page.locator('[data-testid*="login"]')

# Multiple attributes
page.locator(
    'input[type="email"][name="username"]'
)
Useful for: React, Angular, Vue and other applications where stable custom attributes such as data-testid or data-qa are intentionally provided for automation.
6. CSS Combinations

CSS becomes especially useful when multiple conditions need to identify one element.

Combining CSS selectors
# Login button with multiple attributes
page.locator(
    'button.login-button[type="submit"]'
)

# Product card with a specific product ID
page.locator(
    'div.product-card[data-product-id="1001"]'
)

# Enabled button
page.locator(
    'button.primary:not([disabled])'
)

# Input inside checkout form
page.locator(
    '#checkout-form input[name="cardNumber"]'
)
7. CSS Descendant and Child Selectors
DOM relationship selectors
# Any input inside checkout
page.locator("#checkout input")

# Direct child
page.locator("#checkout > button")

# Button inside a card
page.locator(".product-card button")

# Input inside a specific form
page.locator(
    'form#registration-form input[name="email"]'
)
Be careful: Deep CSS chains such as div > div > div > form > div > input are usually fragile. Prefer stable IDs, attributes, semantic locators, or test-specific attributes.
8. CSS Pseudo-Classes

Playwright supports CSS selector functionality, including useful pseudo-class patterns.

Examples
# First matching element
page.locator(".product").first

# Last matching element
page.locator(".product").last

# CSS nth-child
page.locator(".product:nth-child(2)")

# Not disabled
page.locator("button:not([disabled])")

# Checked checkbox
page.locator('input[type="checkbox"]:checked')
Important: Playwright also provides locator APIs such as .first, .last and .filter(). These can be more readable than relying heavily on positional CSS.
9. CSS vs Text-Based Locator

CSS is not always the best locator. The best selector depends on the application's DOM and what makes the element stable and understandable.

CSS Only

page.locator(
    'div.header div.actions button'
).click()

This can become tightly coupled to the DOM structure.

Semantic Locator

page.get_by_role(
    "button",
    name="Save"
).click()

When accessible information is reliable, this can better express the user's intended interaction.

Professional locator strategy: Do not make every locator CSS just because CSS is powerful. Choose the locator that is both stable and communicates the test intent.
10. CSS Locator + Playwright Filtering

One practical advantage of Playwright's locator model is that you can combine a broad CSS locator with locator filtering.

Real-time product table example
from playwright.sync_api import expect

def test_product_price(page):

    page.goto("https://example.test/products")

    product = page.locator(
        ".product-card"
    ).filter(
        has_text="Laptop Pro"
    )

    expect(product).to_be_visible()

    price = product.locator(
        ".product-price"
    )

    expect(price).to_contain_text("$1,299")

Instead of creating one enormous CSS selector, you can identify the component and then narrow it down.

11. Real-Time E-Commerce Example

Consider an e-commerce application with multiple products.

HTML structure
<div class="product-card" data-product-id="1001">
    <h3 class="product-name">Laptop Pro</h3>
    <span class="product-price">$1,299</span>
    <button class="add-to-cart">
        Add to Cart
    </button>
</div>
Playwright
def test_add_laptop_to_cart(page):

    page.goto("https://shop.example.test/products")

    product = page.locator(
        '.product-card[data-product-id="1001"]'
    )

    product.locator(
        ".add-to-cart"
    ).click()

    expect(
        page.locator(".cart-count")
    ).to_have_text("1")
Why this is useful: The product is identified first. The Add to Cart button is then searched inside that product component, reducing accidental interaction with another product's button.
12. Real-Time Banking Example

Suppose a banking application displays transaction rows:

Transaction HTML
<div class="transaction"
     data-transaction-id="TXN-10045">

    <span class="merchant">
        Amazon
    </span>

    <span class="amount">
        -$120.00
    </span>

    <button class="view-details">
        Details
    </button>

</div>
Playwright test
def test_transaction_details(page):

    page.goto("https://bank.example.test/transactions")

    transaction = page.locator(
        '.transaction[data-transaction-id="TXN-10045"]'
    )

    expect(transaction).to_be_visible()

    expect(
        transaction.locator(".merchant")
    ).to_have_text("Amazon")

    transaction.locator(
        ".view-details"
    ).click()

    expect(
        page.locator(".transaction-details")
    ).to_be_visible()
13. CSS Locator with Dynamic Classes

Modern frameworks sometimes generate classes such as:

Example generated HTML
<button class="css-18x7ab2 primary-button">
    Submit
</button>

Avoid Dynamic Class

page.locator(
    ".css-18x7ab2"
).click()

Prefer Stable Class

page.locator(
    ".primary-button"
).click()

Even better, if the application exposes a stable test contract:

Stable automation attribute
page.locator(
    '[data-testid="submit-order"]'
).click()
14. Playwright CSS Locator Chaining

Playwright locators can be scoped step-by-step.

Page
→
Product Card
→
Price
→
Assertion
Locator chaining
product = page.locator(
    ".product-card"
).filter(
    has_text="Laptop Pro"
)

price = product.locator(
    ".product-price"
)

expect(price).to_have_text("$1,299")
This approach makes complex pages easier to understand because each locator represents a meaningful part of the UI.
15. Playwright CSS Locator + Web-First Assertions

A locator should normally be combined with Playwright's web-first assertions rather than immediately checking a boolean.

Avoid

assert page.locator(
    ".success-message"
).is_visible()

This performs an immediate check and does not express Playwright's retrying assertion model.

Preferred

expect(
    page.locator(".success-message")
).to_be_visible()

The assertion waits for the expected UI state within Playwright's configured timeout.

16. CSS Locator vs XPath
Requirement CSS XPath
ID #email //input[@id='email']
Class .login-button //button[contains(@class,'login-button')]
Attribute input[name="email"] //input[@name='email']
Parent traversal Limited / different approach Strong support
Readable simple selectors Usually concise Often more verbose
CSS and XPath are both valid locator technologies. Don't use XPath simply because it is familiar from Selenium.
17. Selenium CSS Locators

Selenium also supports CSS selectors through By.CSS_SELECTOR.

Selenium Python
from selenium.webdriver.common.by import By

driver.get(
    "https://example.test/login"
)

driver.find_element(
    By.CSS_SELECTOR,
    "#email"
).send_keys(
    "john@example.com"
)

driver.find_element(
    By.CSS_SELECTOR,
    "#password"
).send_keys(
    "Password123"
)

driver.find_element(
    By.CSS_SELECTOR,
    'button[type="submit"]'
).click()
18. Playwright vs Selenium - CSS Locator Usage
Area Playwright Selenium
CSS selector API page.locator("css") find_element(By.CSS_SELECTOR, "css")
Locator object Strong locator abstraction WebElement-based interaction
Chaining Natural locator chaining Often requires additional element lookup
Filtering filter() provides expressive narrowing Usually additional lookup or XPath/CSS logic
Waiting Locator actions/assertions integrate with Playwright waiting behavior Explicit waits and Selenium's synchronization APIs are commonly used
Assertions Web-first expect() Typically external assertion framework or direct checks
CSS itself Powerful CSS support Powerful CSS support
Important: CSS selector capability itself is not exclusive to Playwright. Selenium supports CSS selectors too. The practical difference is Playwright's broader locator abstraction, chaining, filtering, actionability handling and web-first assertion model around those selectors.
19. Same Real-Time Scenario - Playwright vs Selenium

Suppose an order page contains:

Order HTML
<div class="order-row" data-order-id="ORD-1001">

    <span class="customer-name">
        John Smith
    </span>

    <span class="status paid">
        Paid
    </span>

    <button class="view-order">
        View
    </button>

</div>

Playwright

Playwright implementation
from playwright.sync_api import expect

def test_order(page):

    page.goto(
        "https://admin.example.test/orders"
    )

    order = page.locator(
        '.order-row[data-order-id="ORD-1001"]'
    )

    expect(order).to_be_visible()

    expect(
        order.locator(".customer-name")
    ).to_have_text("John Smith")

    expect(
        order.locator(".status")
    ).to_have_text("Paid")

    order.locator(
        ".view-order"
    ).click()

Selenium

Selenium implementation
from selenium.webdriver.common.by import By

driver.get(
    "https://admin.example.test/orders"
)

order = driver.find_element(
    By.CSS_SELECTOR,
    '.order-row[data-order-id="ORD-1001"]'
)

customer = order.find_element(
    By.CSS_SELECTOR,
    ".customer-name"
)

status = order.find_element(
    By.CSS_SELECTOR,
    ".status"
)

assert customer.text == "John Smith"

assert status.text == "Paid"

order.find_element(
    By.CSS_SELECTOR,
    ".view-order"
).click()
Both approaches can automate the scenario successfully. Playwright's advantage here is not that CSS selectors magically become more powerful than Selenium's CSS selectors; rather, the surrounding locator API makes complex locator composition and synchronization particularly convenient.
20. Real-Time Dynamic Table Example

Enterprise applications frequently contain tables with many rows. Avoid selecting "the third row" when a business identifier exists.

Fragile

page.locator(
    "table tbody tr:nth-child(3)"
).locator(
    ".view-button"
).click()

If sorting or filtering changes, the third row may no longer represent the intended order.

Business Identifier

order = page.locator(
    "table tbody tr"
).filter(
    has_text="ORD-1001"
)

order.locator(
    ".view-button"
).click()
21. CSS + Data Attributes - Recommended Pattern

If you own the application, consider adding stable automation attributes.

Application HTML
<button
    data-testid="checkout-submit"
    class="primary-button"
>
    Place Order
</button>
Playwright
page.locator(
    '[data-testid="checkout-submit"]'
).click()
A stable test attribute can create an explicit contract between the application and automation suite. The exact attribute convention should be agreed upon by the development and test teams.
22. Avoid Overly Complex CSS

Avoid

page.locator(
    "div.container > div:nth-child(2) > " +
    "div.content > form > div:nth-child(1) > " +
    "input.form-control"
)

Preferred

page.locator(
    'input[name="email"]'
)

Shorter is not automatically better, but stable and intention-revealing selectors are generally easier to maintain.

23. Avoid Hard Waits with CSS Locators

Avoid

import time

page.locator(
    "#submit"
).click()

time.sleep(3)

assert page.locator(
    ".success"
).is_visible()

Preferred

from playwright.sync_api import expect

page.locator(
    "#submit"
).click()

expect(
    page.locator(".success")
).to_be_visible()
Why? Fixed delays make tests slower when the application is fast and can still be unreliable when the application is slower than expected.
24. CSS Locators in Page Object Model

In a professional project, CSS locators can be encapsulated inside page objects.

login_page.py
from playwright.sync_api import Page

class LoginPage:

    def __init__(self, page: Page):
        self.page = page

        self.email = page.locator(
            'input[name="email"]'
        )

        self.password = page.locator(
            'input[name="password"]'
        )

        self.login_button = page.locator(
            'button[type="submit"]'
        )

    def open(self):
        self.page.goto(
            "https://example.test/login"
        )

    def login(self, email, password):
        self.email.fill(email)
        self.password.fill(password)
        self.login_button.click()
test_login.py
from playwright.sync_api import expect

def test_login(page):

    login = LoginPage(page)

    login.open()

    login.login(
        "john@example.com",
        "Password123"
    )

    expect(page).to_have_url(
        r".*/dashboard"
    )
25. Locator Strategy for Professional Projects
1

Role

Use accessible roles when they accurately represent the user interaction.

get_by_role()
2

Label

Great for form fields associated with labels.

get_by_label()
3

Test ID

Useful when the application exposes a stable test contract.

get_by_test_id()
4

CSS

Excellent for stable classes, attributes and structural component targeting.

locator("css")
5

Text

Useful when visible text is the reliable identifier.

get_by_text()
6

XPath

Keep it available for cases where XPath expresses a relationship that other locators do not conveniently express.

locator("xpath=...")
26. Why Playwright CSS Locators Can Feel More Powerful

The CSS selector language itself is not fundamentally exclusive to Playwright. Selenium can also use CSS selectors.

The practical difference comes from how Playwright combines locators with actions, filtering, auto-waiting behavior and web-first assertions.

Capability Practical Playwright Benefit
Locator abstraction Represent an element or component as a reusable locator.
Locator chaining Scope child elements naturally from a parent locator.
Filtering Narrow collections using conditions such as text or nested elements.
Actionability Actions account for relevant element readiness checks.
Web-first assertions Assertions wait for the expected state rather than requiring manual polling.
Readable composition Complex UI components can be modeled as smaller locator expressions.
Multiple locator styles CSS can be combined with role, label, text and test-ID strategies.
27. Complete Real-Time Checkout Example

Imagine a production checkout flow:

Product
→
Cart
→
Checkout
→
Payment
→
Success
Professional Playwright example
from playwright.sync_api import expect

def test_customer_can_complete_checkout(page):

    # Product
    page.goto(
        "https://shop.example.test/products"
    )

    product = page.locator(
        '.product-card[data-product-id="1001"]'
    )

    product.locator(
        ".add-to-cart"
    ).click()

    # Cart
    expect(
        page.locator(".cart-count")
    ).to_have_text("1")

    page.locator(
        '[data-testid="cart-link"]'
    ).click()

    # Checkout
    page.locator(
        '[data-testid="checkout-button"]'
    ).click()

    page.locator(
        'input[name="address"]'
    ).fill(
        "100 Main Street"
    )

    page.locator(
        'input[name="city"]'
    ).fill(
        "Pune"
    )

    # Payment
    page.locator(
        '[data-testid="payment-card"]'
    ).fill(
        "4111111111111111"
    )

    page.locator(
        '[data-testid="place-order"]'
    ).click()

    # Validation
    expect(
        page.locator(".order-success")
    ).to_be_visible()

    expect(
        page.locator(".order-status")
    ).to_have_text("Confirmed")
28. Common CSS Locator Mistakes
29. CSS Locator Best Practices
30. Quick CSS Locator Reference
Goal Playwright Example
ID page.locator("#email")
Class page.locator(".login-button")
Attribute page.locator('[name="email"]')
Multiple attributes page.locator('input[type="email"][name="email"]')
Descendant page.locator(".checkout input")
Direct child page.locator(".checkout > button")
Not disabled page.locator("button:not([disabled])")
Test ID page.locator('[data-testid="save"]')
First locator page.locator(".item").first
Last locator page.locator(".item").last
Filtered component page.locator(".card").filter(has_text="Laptop")
31. Final Mental Model
Find stable element
→
Create Locator
→
Scope / Filter
→
Perform Action
→
Web-First Assertion
Remember: CSS selectors are a powerful part of Playwright, but a robust Playwright strategy is not "CSS everywhere." The goal is to choose the most stable, readable locator for each UI element and then let Playwright's locator and assertion model handle synchronization.
32. Interview Summary

Question: Does Selenium support CSS selectors?

Yes. Selenium has long supported CSS selectors through By.CSS_SELECTOR.

Question: Is Playwright CSS syntax fundamentally different?

The underlying CSS selector concepts are standard browser CSS concepts. The practical difference is the Playwright locator API surrounding them.

Question: Why can Playwright feel more powerful?

Playwright combines CSS selectors with locator chaining, filtering, actionability checks, automatic waiting behavior and web-first assertions.

Question: Should I always use CSS?

No. Use the locator strategy that gives you the best combination of stability, readability and user intent.

Question: What is a strong real-world CSS selector?

Example
page.locator(
    '[data-testid="checkout-submit"]'
)
Professional answer: "I use CSS selectors when the DOM exposes stable IDs, classes or automation attributes. I combine them with Playwright's locator chaining and filtering when I need to scope an element inside a component. I don't use CSS indiscriminately; for accessible user-facing controls, role or label locators may communicate intent better."