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
getByPlaceholder
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
Prefer User-Facing Attributes
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' );
Use Test IDs for Stability
Add data-testid for elements without good text or role. // Stable selector
await page . getByTestId ( 'checkout-button' ). click ();
Avoid XPath When Possible
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")]' );
Be Specific But Not Brittle
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 );
CSS is fastest - Browser-native
Text selectors are slower - Require content scanning
XPath is slowest - Complex evaluation
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