How to Locate Element by Class in Playwright in 2026

Explore the most effective methods to locate elements by class in Playwright for 2026. Boost your testing efficiency with these techniques!

Written by Ashwani Pathak Ashwani Pathak
Reviewed by Sujay Sawant Sujay Sawant
Last updated: 28 July 2026 14 min read

Key Takeaways

  • Locate an element by class in Playwright with page.locator('.className'), then scope or filter the locator when the same class matches multiple elements.
  • Class selectors are most reliable when they use stable, meaningful classes rather than generated names, styling classes, temporary states, or DOM position.
  • Repeated .first(), .nth(), long CSS chains, and fixed waits often point to a locator that needs to be refined or replaced.

Locating the right element is a basic part of writing any Playwright test. If you want to click a button, fill a form, or verify content on a page, Playwright first needs a way to identify that element.

One way to do this is with a class name. If an element has a class such as submit-button, you can use a CSS class selector with Playwright’s locator API to target it. The syntax is simple, but classes are often shared by multiple elements and may change as the UI evolves.

To use class selectors reliably, you need to know how Playwright handles matching elements, how to narrow a locator when a class is not unique, and when another locator is a better choice.

Why Use Class Selectors in Playwright

Class selectors are useful when a stable class already gives you a direct way to identify an element or a group of related elements. They also work well when you need the flexibility of CSS to scope or combine selectors. In practice, you may choose them for cases such as:

  • Working with repeated components: Target a shared set of elements such as product cards, table rows, or navigation items before filtering or iterating over the matches.
  • Scoping elements within a component: Start with a component-level class and locate a specific element inside it. This helps when the same button, link, or field appears in several parts of the page.

Why use Class Selectors in Playwright

  • Combining selector conditions: Use multiple classes or combine a class with an element type or attribute when one class alone does not identify the required element.
  • Testing applications without dedicated test attributes: Use an existing stable class when the application does not expose data-testid or another purpose-built testing attribute.
  • Targeting stable DOM hooks: Some applications use semantic classes that remain consistent across UI changes. These can provide practical locators when they are not tied only to visual styling.

Understanding Locators in Playwright

Playwright offers a wide variety of ways to locate elements on a page. Some of the most frequently used Playwright locators include:

  • Built-in Locator Methods (getByRole, getByText, etc.): These methods help identify elements based on their roles, text content, or other attributes, making tests more resilient to changes in class names.
  • Using CSS Selectors and Class Names: CSS selectors provide a straightforward method to target elements by their classes, IDs, or other attributes. While powerful, class-based selectors should be used thoughtfully to avoid selecting multiple elements when only one is needed.

How to Locate an Element by Class in Playwright

Locating elements by class name is one of the most direct approaches in Playwright. The syntax for using class names with Playwright is as follows:

page.locator(‘.className’)

This simple command will find the first element with the specified class. If there are multiple elements with the same class, Playwright will return the first match.

How to pick the right Playwright locator

Handling Multiple Elements with the Same Class

When multiple elements share the same class name, it’s important to consider how to handle these cases. You can use more specific selectors or combine them with other attributes (e.g., text, role) to narrow down the search.

Additionally, Playwright allows you to interact with all matching elements using methods like .locator(), .first(), or .nth() to specify which element to target.

Advanced Techniques When Using Class Selectors

Once a simple class selector is not enough, the next step is usually not to build a longer CSS path. Playwright gives you several ways to keep the class as the starting point and narrow the match based on the element you actually need.

1. Combine a class with text

If several elements share the same class, filter them by visible content instead of relying on position.

const plan = page.locator('.pricing-card').filter({

  hasText: 'Pro'

});

await plan.getByRole('button', { name: 'Start free trial' }).click();

This works well for repeated components such as cards, rows, and list items where the class identifies the component type and the text identifies the specific instance.

2. Use one class to scope another locator

A class can also act as a boundary for the rest of the search.

const checkout = page.locator('.checkout-panel');


await checkout.getByLabel('Email').fill('user@example.com');

await checkout.getByRole('button', { name: 'Pay now' }).click();

Here, the class is not used to locate every element directly. It limits the search to the relevant part of the page so the locators inside that section stay clear and less likely to collide with similar elements elsewhere.

3. Match elements that have another element inside them

The has option is useful when the element you need is best identified by one of its descendants.

const product = page.locator('.product-card').filter({

  has: page.getByText('Noise Cancelling Headphones')

});

await product.locator('.add-to-cart').click();

This is often cleaner than constructing a CSS selector that tries to describe the full relationship between the parent and child.

4. Combine classes only when each one adds meaning

Multiple classes can make a selector more precise:

page.locator('.button.primary');

But this is useful only when both classes are stable. Adding more classes does not automatically make a locator better. If one of them exists only for styling, the selector becomes more sensitive to frontend changes without gaining much test value.

5. Use position only when position is part of the test

Methods such as .first(), .last(), and .nth() are useful when the scenario genuinely depends on order.

const rows = page.locator('.result-row');

await expect(rows.nth(0)).toContainText('Passed');

They are less suitable when position is being used only because the locator matches too many elements. In that case, refine the locator based on the component, content, or another stable property instead.

Common Pitfalls and How to Avoid Them

Most problems with class-based locators appear after the test has already been working for some time. The UI changes, another instance of a component gets added, or a class that looked permanent turns out to be generated at build time. When a class-based locator starts causing failures, these are some of the first issues worth checking.

1. Using a generated class as a stable locator

Suppose you inspect a button and see this:

<button class="css-1x2ab3">Checkout</button>

You could write:

await page.locator('.css-1x2ab3').click();

The test may pass today and fail after a new build because the generated class has changed. This is common with CSS-in-JS libraries, CSS Modules, and other setups where the rendered class is produced during the build process.

Before relying on a class in a locator, check where it comes from. A class that happens to remain unchanged across a few test runs should not be treated as a stable test hook unless the application actually controls that value.

2. Fixing multiple matches with .first()

Consider a test that originally has one checkout button:

await page.locator('.checkout-button').click();

Later, a sticky checkout bar is added to the page. There are now two elements with .checkout-button, and Playwright reports a strict mode violation when the test tries to click the locator.

The quickest fix is often this:

await page.locator('.checkout-button').first().click();

The test may pass again, but the selector still does not express which checkout button the test needs. A better fix is to scope it to the relevant component:

const cart = page.locator('.cart-summary');

await cart.locator('.checkout-button').click();

I would reserve .first() and .nth() for cases where position is genuinely part of what the test is verifying. They should not be used simply to silence an ambiguous locator.

3. Adding a timeout when the real problem is element state

Suppose a menu opens with an animation and a test occasionally fails while interacting with an item. Adding a fixed wait can appear to solve the problem:

await page.waitForTimeout(2000);

await page.locator('.menu-item').click();

Now every test run waits two seconds whether the element needs that time or not. More importantly, the delay does not address why the interaction was failing.

Playwright already waits for relevant actionability conditions before performing actions such as click(). If an interaction still fails, check whether the element is hidden, covered by another element, disabled, or being replaced during a UI update. Fixing that synchronization point is more reliable than choosing an arbitrary delay.

4. Depending on a class that represents temporary state

Classes such as .active, .open, and .selected describe the current state of an element rather than its identity.

For example:

const tab = page.locator('.tab.active');

This is appropriate when the test needs to verify or interact with the tab that is currently active. It becomes unreliable when the same selector is used as the permanent way to identify a specific tab because the locator stops matching as soon as its state changes.

The distinction is simple: use state classes to locate a state when that state matters to the test. Do not use them as a substitute for the element’s identity.

Best Practices for Class-based Locators

A class selector does not become reliable just because it uniquely matches an element today. Before using one across a test suite, look at what the class represents and how likely it is to survive routine frontend changes.

1. Prefer classes that are part of the application structure

A class such as .product-card or .checkout-form often represents an actual component on the page. A class such as .mt-4, .flex, or .css-1a2b3c usually represents styling or generated output.

Tests tied to styling classes can fail after a visual change even when the behavior under test remains exactly the same. When I review class-based locators, the first thing I check is whether the class describes something meaningful in the application or simply reflects how the element currently looks.

2. Narrow the search from a stable parent

Shared classes are common and are not necessarily a problem. If every product has an .add-to-cart button, first locate the required product card and search within it.

const product = page.locator('.product-card').filter({

  hasText: 'Wireless Headphones'

});


await product.locator('.add-to-cart').click();

This keeps the relationship between the product and its button visible in the test. It also avoids encoding the exact DOM hierarchy into one long CSS selector.

3. Do not use .first() to hide an ambiguous locator

If a locator unexpectedly matches five elements, adding .first() can make the test pass without solving the underlying problem. The test now depends on whichever matching element happens to appear first.

I use .first() or .nth() when order is part of the scenario. If the test is supposed to interact with a specific element, the locator should identify that element through its component, content, role, or another stable property.

4. Keep DOM structure out of the selector where possible

Consider a selector such as:

page.locator('.products > div:nth-child(2) > div > button')

It may identify the correct button, but it also captures assumptions about the current page structure. Adding a wrapper or moving the button within the component can break the test without changing the feature itself.

A scoped locator usually leaves more room for the frontend structure to change:

const product = page.locator('.product-card').filter({

  hasText: 'Wireless Headphones'

});


await product.getByRole('button', { name: 'Add to cart' }).click();

The class identifies the component. The role and accessible name identify the action. I generally prefer this kind of composition over making the CSS selector increasingly specific.

5. Let Playwright handle normal waiting

Adding waitForTimeout() before a class-based locator should not be the default response to an intermittent failure.

await page.waitForTimeout(2000);

await page.locator('.checkout-button').click();

Playwright already performs actionability checks before actions such as click(). If the interaction still fails intermittently, investigate the state the application is waiting on rather than increasing the delay.

When a test genuinely needs to wait for a specific UI state, express that condition directly with a locator or assertion. A test that waits for the expected state is easier to reason about than one that simply waits for time to pass.

Maintaining and Refactoring Class-based Locators

Class-based locators become harder to maintain when the same selector is spread across many tests or when developers keep adding small fixes every time the UI changes. Refactoring is worth considering before those fixes become part of the test’s permanent design.

1. Know when a locator needs refactoring

A locator may start like this:

await page.locator('.primary-button').click();

After a few UI changes, it may end up like this:

await page.locator('.primary-button').nth(2).click();

The second version works only while the required button remains the third match. If you find yourself adding .first(), .nth(), longer CSS chains, or additional classes just to keep an existing test running, check whether the locator should be replaced instead.

For example, you could scope the action to the relevant part of the page:

const checkout = page.locator('.checkout-summary');

await checkout.getByRole('button', { name: 'Place order' }).click();

Sometimes the class itself is the problem. If it changes regularly because it is tied to styling or generated during the build, replacing it with another class may only delay the next failure. This is where I would consider a role, label, test ID, or another stable attribute instead.

2. Manage shared locators without over-abstracting

When the same class-based locator appears across many tests, keeping it in a page or component object can make future changes easier to manage.

class CheckoutPage {

  constructor(page) {

    this.summary = page.locator('.checkout-summary');

    this.placeOrderButton = this.summary.getByRole('button', {

      name: 'Place order'

    });

  }



  async placeOrder() {

    await this.placeOrderButton.click();

  }

}

If the checkout markup changes, the shared locator can be updated in one place. But that does not mean every locator needs to be moved into a page object.

A selector used once in a small test may be easier to understand when it stays next to the interaction. I generally centralize locators when they are reused across tests or when several actions depend on the same component. Creating abstractions for every selector can make simple tests harder to follow.

3. Review related tests when a component changes

A component change can affect more tests than the first failure suggests. Suppose one test locates a product with .product-card, another uses .product-grid > div:nth-child(3), and a third relies on .add-button.first().

If the product card has been redesigned, fixing each test only when it fails means dealing with the same change several times. Review the tests that interact with that component together and check whether they still use selectors that match the current implementation.

This is also a useful time to standardize inconsistent locators. If several tests interact with the same component in different ways, decide which selectors are stable and update the related tests while the component change is still fresh.

Conclusion

Class selectors work well in Playwright when the underlying class is stable and gives you a clear way to reach the element you need. You can also use them to work with repeated components or scope other locators to a specific part of the page. The key is to avoid treating every available class as a reliable test hook.

As your test suite grows, watch for class locators that depend on position, generated names, or repeated workarounds. I recommend refining or replacing these before they start causing failures across multiple tests. Use class selectors where they make sense, and switch to roles, labels, or test IDs when they give you a more reliable target.

Version History

  1. Jul 23, 2026 Current Version

    Revamped the article with updated information, practical examples, and deeper technical insights while replacing generic AI-style explanations with more useful, expert-led guidance.

    Sujay Sawant
    Reviewed by Sujay Sawant Lead Engineer
Tags
Playwright
Ashwani Pathak
Ashwani Pathak

Automation Expert

Ashwani has been working on automation products for 5+ years and has a deep understanding of what teams need to run tests reliably at scale. He brings a sharp product perspective on how automation fits into modern development workflows.

Class Locators Breaking in CI?
Debug Playwright tests across real browser and OS combinations.