Skip to main content

Overview

Playwright uses selector engines to find elements in the page. The selector system is highly flexible and supports multiple strategies.

Built-in Selector Engines

From server/selectors.ts:32-48:
this._builtinEngines = new Set([
  'css', 'css:light',
  'xpath', 'xpath:light',
  '_react', '_vue',
  'text', 'text:light',
  'id', 'id:light',
  'data-testid', 'data-testid:light',
  'nth', 'visible', 'internal:control',
  'internal:has', 'internal:has-not',
  'internal:has-text', 'internal:has-not-text',
  'role', 'internal:attr', 'internal:label',
  'aria-ref'
]);

CSS Selectors

Standard CSS selectors work out of the box:
// By ID
await page.click('#submit-button');

// By class
await page.click('.btn-primary');

// By attribute
await page.click('[data-test="login"]');

// Combinators
await page.click('div.container > button.submit');

// Pseudo-classes
await page.click('button:not(.disabled)');

CSS:Light

Pierces shadow DOM:
// Regular CSS stops at shadow boundaries
await page.click('css=custom-element button');

// css:light pierces shadow DOM
await page.click('css:light=custom-element button');

Text Selectors

Find elements by their text content:
// Exact text match
await page.click('text="Sign In"');

// Substring match
await page.click('text=Sign');

// Case insensitive
await page.click('text=/sign in/i');

// Text in specific element
await page.click('button:has-text("Submit")');
Text selectors are normalized: trimmed and whitespace collapsed.

XPath Selectors

// XPath syntax
await page.click('xpath=//button[@id="submit"]');

// Shorthand
await page.click('//button[@id="submit"]');

// XPath:light (pierces shadow DOM)
await page.click('xpath:light=//button');

Role Selectors

Find elements by ARIA role (accessibility-first):
// By role
await page.getByRole('button').click();

// With name
await page.getByRole('button', { name: 'Sign In' }).click();

// With attributes
await page.getByRole('textbox', {
  name: 'Email',
  checked: true,
  disabled: false
}).fill('[email protected]');
From utils/isomorphic/locatorUtils.ts:
export function getByRoleSelector(role: string, options: ByRoleOptions = {}): string {
  return `internal:role=${role}[name=${JSON.stringify(options.name || '')}]`;
}
Supported roles:
  • button, checkbox, radio, textbox
  • link, heading, img, list, listitem
  • table, row, cell, dialog
  • And all ARIA roles…

Test ID Selectors

Recommended for test automation:
// Default attribute: data-testid
await page.getByTestId('submit-button').click();

// Custom attribute
playwright.selectors.setTestIdAttribute('data-test-id');
await page.getByTestId('submit-button').click();
HTML:
<button data-testid="submit-button">Submit</button>
Use test IDs for stable, maintainable selectors that don’t break when styling changes.

Locator Methods

getByRole

await page.getByRole('button', { name: 'Submit' }).click();

getByText

await page.getByText('Welcome').click();
await page.getByText(/welcome/i).click();
await page.getByText('Welcome', { exact: true }).click();

getByLabel

await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel(/e-?mail/i).fill('[email protected]');

getByPlaceholder

await page.getByPlaceholder('Enter your email').fill('[email protected]');

getByAltText

await page.getByAltText('Profile picture').click();

getByTitle

await page.getByTitle('Close').click();

getByTestId

await page.getByTestId('submit').click();

Combining Selectors

Chaining (>>)

// CSS then text
await page.click('article >> text=Read more');

// Multiple chains
await page.click('div.modal >> button >> text=OK');

Filtering with :has()

// Button containing specific text
await page.click('button:has-text("Submit")');

// Article containing specific element
await page.click('article:has(h2:text("Breaking News"))');
From client/locator.ts:44-70:
constructor(frame: Frame, selector: string, options?: LocatorOptions) {
  this._frame = frame;
  this._selector = selector;
  
  if (options?.hasText)
    this._selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`;
  
  if (options?.has) {
    const locator = options.has;
    if (locator._frame !== frame)
      throw new Error(`Inner "has" locator must belong to the same frame.`);
    this._selector += ` >> internal:has=` + JSON.stringify(locator._selector);
  }
}

And Combinator

// Element matching both selectors
const locator = page.locator('button').and(page.locator('[type="submit"]'));
await locator.click();

Or Combinator

// Element matching either selector
const locator = page.locator('button').or(page.locator('input[type="submit"]'));
await locator.click();

React & Vue Selectors

Find elements by React/Vue component names:
// React component
await page.locator('_react=MyButton').click();

// React component with props
await page.locator('_react=MyButton[disabled=false]').click();

// Vue component
await page.locator('_vue=MyButton').click();
React/Vue selectors require components to be in development mode or have displayName set.

Layout Selectors

Positional

// First matching element
await page.locator('button').first().click();

// Last matching element
await page.locator('button').last().click();

// Nth element (0-indexed)
await page.locator('button').nth(2).click();

Visibility

// Only visible elements
const locator = page.locator('button', { visible: true });

// Hidden elements
const locator = page.locator('button', { visible: false });

Filtering

// Filter by text
const locator = page.locator('button', {
  hasText: 'Submit'
});

// Filter by child element
const locator = page.locator('article', {
  has: page.locator('img')
});

// Exclude by text
const locator = page.locator('button', {
  hasNotText: 'Cancel'
});

// Exclude by child
const locator = page.locator('article', {
  hasNot: page.locator('.ad')
});

Strict Mode

By default, actions require exactly one matching element:
// Throws if multiple buttons match
await page.click('button');

// Disable strict mode (not recommended)
await page.click('button', { strict: false });

// Better: make selector more specific
await page.click('button.submit');
Strict mode prevents accidental interactions with the wrong element.

Selector Examples

By Attribute

// Single attribute
await page.click('[data-test="login"]');

// Multiple attributes
await page.click('[type="submit"][disabled="false"]');

// Attribute contains
await page.click('[class*="btn"]');

// Attribute starts with
await page.click('[id^="submit"]');

By Relationship

// Parent-child
await page.click('form > button');

// Descendant
await page.click('form button');

// Adjacent sibling
await page.click('label + input');

// General sibling
await page.click('h2 ~ p');

Complex Selectors

// Multiple conditions
await page.click('button.primary:not(.disabled):has-text("Submit")');

// Chained selectors
await page.click('div.modal >> form >> button[type="submit"]');

// With filters
const locator = page.locator('article', {
  has: page.locator('h2', { hasText: 'News' })
});
await locator.locator('a.read-more').click();

Custom Selector Engines

Register custom selector engines:
// Register custom engine
await playwright.selectors.register('tag', {
  // Query one element
  query(root, selector) {
    return root.querySelector(selector);
  },
  // Query all elements
  queryAll(root, selector) {
    return Array.from(root.querySelectorAll(selector));
  }
});

// Use custom engine
await page.click('tag=button');

Selector Best Practices

Use roles, labels, and text that users see.
// Good
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('[email protected]');

// Avoid
await page.click('.btn-submit-xyz-123');
Add data-testid for elements without good text or role.
// Stable selector
await page.getByTestId('checkout-button').click();
CSS and text selectors are more readable and maintainable.
// Better
await page.click('button:has-text("Submit")');

// Avoid
await page.click('//button[contains(text(), "Submit")]');
Balance specificity with maintainability.
// Too brittle
await page.click('div.container > div:nth-child(3) > button.btn-primary-xyz');

// Better
await page.click('button[type="submit"]');

// Best
await page.getByRole('button', { name: 'Submit' }).click();

Debugging Selectors

// Get all matching elements count
const count = await page.locator('button').count();
console.log(`Found ${count} buttons`);

// Get element attributes
const text = await page.locator('button').textContent();
const isVisible = await page.locator('button').isVisible();

// Highlight element (in headed mode)
await page.locator('button').highlight();

// Get computed selector
const selector = await page.locator('button').toString();
console.log(selector);

Selector Performance

  1. CSS is fastest - Browser-native
  2. Text selectors are slower - Require content scanning
  3. XPath is slowest - Complex evaluation
  4. Layout selectors - May require style computation
// Fast
await page.click('#submit');

// Slower
await page.click('text=Submit');

// Slowest
await page.click('//button[contains(text(), "Submit")]');

Next Steps

Auto-waiting

How Playwright waits for elements

Locators

Locator API reference

Test Isolation

Ensuring independent tests

Build docs developers (and LLMs) love