QA & Testing

CSS Selector Generator for Playwright, Cypress & Selenium

Paste any HTML snippet, click an element in the tree, and instantly get ranked selectors for Playwright (getByRole, getByLabel, getByTestId), Cypress (cy.get, cy.contains), Selenium (By.ID, By.CSS_SELECTOR, By.XPATH), CSS, and XPath. Every selector is rated Best → Good → OK → Fragile so you know which will survive a UI refactor.

Playwright, Cypress, Selenium, and CSS Selector Strategies Compared

The selector you choose is the single biggest factor in how often your tests break on legitimate UI changes. A test that fails because a developer renamed a CSS class is a false positive — it costs time, erodes trust in the test suite, and eventually gets deleted. A test that fails because the "Submit" button disappeared from the page is a true positive — exactly what automation is for.

This tool generates selectors from your actual HTML and rates each one by resilience. Paste a snippet, click an element, and get ranked selectors for Playwright, Cypress, Selenium, CSS, and XPath — each labelled Best, Good, OK, or Fragile with a plain-English reason.

Playwright Locator Priority — From Most to Least Resilient

Playwright's official documentation defines a locator priority. Every locator lower on the list breaks more often in practice:

LocatorExampleResilienceBest for
getByRole()page.getByRole('button', { name: 'Submit' })BestAny interactive element — buttons, links, inputs, checkboxes
getByLabel()page.getByLabel('Email address')BestForm fields with associated <label> elements
getByPlaceholder()page.getByPlaceholder('Enter email')GoodInputs without labels; breaks if placeholder text is localized
getByText()page.getByText('Forgot password?')GoodStatic visible text in links, labels, paragraphs
getByAltText()page.getByAltText('Company logo')GoodImages; alt text changes rarely
getByTestId()page.getByTestId('login-btn')BestWhen role/label is ambiguous; requires adding data-testid
locator(css)page.locator('#submit-btn')Good (id) / OK (class)IDs are reliable; class selectors break when styles change
locator(xpath)page.locator('//button[1]')FragileLast resort; structural paths break on layout changes

How to Add data-testid to Your Components

The fastest way to make your markup testable is to add data-testid to interactive elements. Name them in kebab-case, scoped to the feature:

<!-- HTML -->
<button data-testid="checkout-place-order">Place order</button>

<!-- React -->
<Button data-testid="checkout-place-order">Place order</Button>

<!-- Vue -->
<button :data-testid="testId">Place order</button>

<!-- Playwright -->
await page.getByTestId('checkout-place-order').click();

<!-- Cypress -->
cy.get('[data-testid="checkout-place-order"]').click();

Keep a naming convention: {feature}-{element}-{role}. This prevents two teams independently creating data-testid="submit" on different forms in the same page. If you use a component library, add testId as a prop passed through to the root element.

CSS Selector Quality — What Makes a Selector Stable

Not all CSS selectors are equal. Their resilience depends on how likely the targeted attribute is to change when a developer touches the component:

Selector typeExampleWhy it breaks
data-testid[data-testid="email-input"]Never — dedicated test attribute, only changes intentionally
id#emailRarely — IDs change when feature is renamed or refactored
aria-label[aria-label="Search"]Rarely — accessibility attribute, treated seriously by teams
nameinput[name="email"]Occasionally — form name changes when API schema changes
Semantic classbutton.btn-primaryFrequently — class names change with design system updates
Utility classbutton.flex.items-centerVery often — Tailwind/utility classes are layout-only, not identity
Structural pathform > div:nth-of-type(2) > inputAny layout change — adding a wrapper div breaks it silently

XPath — When to Use It and When to Avoid It

XPath is more powerful than CSS but less readable and slower to execute. Use it only when CSS cannot express the relationship you need:

  • Parent selection: XPath can find a parent — //label[contains(., 'Email')]/.. selects the container of a label. CSS has no parent selector.
  • Text anywhere in subtree: //button[contains(., 'Submit')] matches a button whose text is nested inside a <span>. CSS :has() is an alternative but has limited browser support in older test runners.
  • Attribute value matching: //input[starts-with(@id, 'user-')] matches dynamic IDs with a known prefix.

Avoid absolute XPath like /html/body/div[1]/form/div[2]/input — it encodes the full DOM structure and breaks on any layout change. Use relative XPath starting with // and filter by attribute whenever possible.

Cypress Best Practices for Selectors

Cypress's official testing practices mirror Playwright's: prefer attributes that survive refactoring over structural paths. The Cypress team recommends data-cy or data-testid as explicit test hooks:

// Best — dedicated attribute
cy.get('[data-testid="login-btn"]').click();

// Good — ID selector
cy.get('#login-btn').click();

// Good — text match with tag narrowing
cy.contains('button', 'Sign in').click();

// OK — class selector (breaks if styles change)
cy.get('.btn-primary').click();

// Avoid — structural nth-child
cy.get('form > div:nth-child(3) > button').click();

The cy.contains(tag, text) form is more reliable than bare cy.contains(text) because it narrows the match to a specific element type, preventing false matches on other text nodes in the page.

ARIA Roles and Accessible Names — Why They Matter for Tests

When Playwright's getByRole('button', { name: 'Sign in' }) fails to find an element, it usually means one of three things: the element is genuinely missing, the element lost its accessible name (a bug), or the element's ARIA role is wrong (also a bug). This is the key advantage of role-based locators — they make your test suite a passive accessibility audit.

Every HTML element has an implicit ARIA role. Interactive elements have these defaults:

HTML elementImplicit roleName source
<button>buttonText content or aria-label
<a href>linkText content or aria-label
<input type="text">textboxAssociated label or aria-label
<input type="checkbox">checkboxAssociated label or aria-label
<select>comboboxAssociated label or aria-label
<img>imgalt attribute
<nav>navigationaria-label
<h1>–<h6>headingText content

Workflow: From DevTools to Test

  1. Open your application and navigate to the page under test.
  2. Open DevTools (F12 → Elements), right-click the element you want to test, and choose Copy → Copy outerHTML.
  3. Paste the HTML into the input above and click Parse HTML.
  4. Click the target element in the tree. Switch between Playwright, Cypress, Selenium, CSS, and XPath tabs to see all available selectors.
  5. Pick a Best-rated option if one exists. If not, use a Good-rated selector and consider adding a data-testid to the component to get a Best-rated selector in the future.
  6. Click Copy and paste directly into your test file.

FAQ

Common questions

What is the best locator strategy for Playwright?

Playwright recommends this priority order: (1) getByRole — uses ARIA role + accessible name, survives most UI refactors; (2) getByLabel — ideal for form fields linked to a label element; (3) getByPlaceholder — fallback for inputs without labels; (4) getByText — for visible text in buttons and links; (5) getByTestId — explicit test attribute, requires adding data-testid to your markup; (6) locator() with CSS/XPath — last resort. The top three strategies are resilient because they reflect how users perceive the UI rather than its DOM structure.

When should I use data-testid instead of getByRole?

Use data-testid when: (1) the element has no meaningful ARIA role or accessible name; (2) you have multiple identical elements (e.g., three identical "Delete" buttons in a list) and need to target a specific one by its position or context; (3) your markup changes frequently and you want an explicit, stable anchor that test engineers control. For most interactive elements — buttons, links, form fields — getByRole or getByLabel is preferred because it doubles as an accessibility audit: if the locator breaks, it usually means your markup lost its accessible name too.

How do I add stable selectors to my React or Vue components?

The simplest approach is to add data-testid attributes to key interactive elements: <button data-testid="submit-order">Place order</button>. For React you can pass testId as a prop from your design system. For Vue, use :data-testid="testId" in the template. In a monorepo, keep testId values kebab-case and scoped to the feature (e.g., "checkout-submit-btn") to avoid collisions. Avoid using auto-generated IDs from CSS-in-JS libraries as selectors — they change on every build.

What is the difference between CSS selectors and XPath?

CSS selectors traverse the DOM top-down and cannot select parent elements — they only move forward in the document. XPath is an expression language that can traverse in any direction: parent, ancestor, sibling, and can filter by text content natively (//button[contains(., "Submit")]). CSS selectors are faster in browsers because the engine is heavily optimised for them. XPath is more powerful for complex relationships but less readable. In Playwright and Cypress, CSS selectors are preferred for performance and readability; XPath is useful for legacy test suites or when you need to select by text in a context where getByText is not available.

Why does the generator show a "Fragile" warning on the full CSS path?

A full structural path like form > div:nth-of-type(2) > input encodes the exact position of the element in the DOM tree. Any change to the surrounding layout — adding a new wrapper div, reordering sections, or inserting a component above the target — silently breaks the selector without changing the element itself. The element still exists and still works, but your test can no longer find it. Use structural paths only as a last resort when no attribute-based selector exists.

Can I use this tool for dynamically generated elements?

Yes — paste the HTML that the page renders at the time you want to test it. Open DevTools → Elements, right-click the element, Copy → Copy outerHTML, then paste it here. If the element is rendered by JavaScript after a user action (click, API response), capture the HTML after that action. The tool generates selectors based on the attributes present in the pasted HTML; if your app generates elements with dynamic IDs or random class names, those will appear as "Fragile" — the recommendation is to add a data-testid to the component that renders that element.

What is aria-label and why does Playwright prefer it?

aria-label is an HTML attribute that provides a text label for an element that screen readers announce to users. Playwright's getByRole and getByLabel both use it to identify elements — an <input aria-label="Search products" /> is found by page.getByRole("textbox", { name: "Search products" }). Preferring ARIA attributes as selectors has a useful side effect: if your test can't find an element, it often means the element is also invisible to screen reader users, which is an accessibility bug. Writing tests this way makes your test suite a lightweight accessibility checker.

Is the HTML I paste sent to a server?

No. The entire tool runs in your browser using JavaScript's built-in DOMParser API. Your HTML never leaves your device — there are no server requests, no logging, and no analytics on the content you paste. You can verify this by opening DevTools → Network while using the tool. The tool also works fully offline once the page is loaded.

More in QA & Testing