Dynamic IDs and Generated Classes
React, Angular, Vue and CSS-in-JS generate ids and class names that change on every build. Learn to spot them, and the partial-match and anchor strategies that survive them.
Dynamic IDs and Generated Classes
Modern front ends generate attributes. Emotion and styled-components produce classes like css-1x2y3z. Angular Material numbers its inputs mat-input-3. React Select emits react-select-2-input. Some builds hash everything: _button_4f2ba_7. A locator that copies one of these values works today and fails after the next deploy.
Recognising a generated value
Treat a value as unstable if it has any of these traits:
| Trait | Examples |
|---|---|
| Hex or base-36 hash | css-1x2y3z, sc-bdVaJa, _root_8f3a1_2 |
| Trailing counter | mat-input-3, react-select-2-input, radix-:r1: |
| Random-looking token | id="yui_3_17_2_1_1650000000000_123" |
| Framework prefix plus number | ember123, ext-gen42, ui-id-7 |
| Utility-only classes | flex items-center px-4 py-2 (Tailwind) |
Utility classes are stable but not meaningful: px-4 is on hundreds of elements. They fail the uniqueness test rather than the stability test.
Strategy 1: use a different attribute
Before writing a clever XPath, look for something the framework did not generate:
//input[@name='email'] name is set by the developer
//button[@aria-label='Close dialog'] accessibility labels are stable
//*[@data-testid='submit'] test ids exist precisely for this
//input[@type='password'] semantic attributes
//a[@href='/account'] routes change rarely
Nine times out of ten there is a stable attribute nearby.
Strategy 2: match the stable part
When the attribute mixes a stable stem with a generated suffix, match only the stem:
<input id="mat-input-3" placeholder="Email">
<div class="css-1x2y3z-Control">
<button id="btn-save-8f3a">Save</button>
//input[starts-with(@id, 'mat-input-')] still not unique, combine with more
//div[contains(@class, '-Control')] library suffix is stable
//button[starts-with(@id, 'btn-save')] stem is stable
Combine with context to reach uniqueness:
//label[normalize-space()='Email']/following::input[starts-with(@id, 'mat-input-')][1]
//div[contains(@class, '-Control')][.//input[@name='country']]
Strategy 3: anchor to something stable and walk
Generated markup usually still has human-authored text or structure somewhere. Anchor on that and navigate:
//h3[normalize-space()='Shipping address']/following::input[@name='postcode'][1]
//label[contains(., 'Remember me')]/preceding-sibling::input[@type='checkbox']
//*[@data-testid='cart-row'][.//span[normalize-space()='USB-C Hub']]//button[contains(., 'Remove')]
See Anchoring to Stable Context for the full technique.
Strategy 4: ignore the attribute entirely
Sometimes the right answer is to match on role and text and let the generated attributes fall away:
//button[normalize-space()='Save']
//*[@role='tab'][normalize-space()='Billing']
//li[@role='option'][normalize-space()='Germany']
Roles come from ARIA and are stable across styling changes. This is also the direction Playwright and Testing Library push you in.
Avoiding the trap: “Copy XPath” in DevTools
Chrome’s Copy XPath produces something like //*[@id="mat-input-3"] or /html/body/div[2]/div/main/form/div[3]/input. Both are fragile: the first depends on a counter, the second on layout. Treat the copied value as a starting point for locating the element, then write your own.
Class matching that does not over-match
contains(@class, 'btn') also matches btn-danger, btn-outline and subtn. When the class name matters, match the token:
//button[contains(concat(' ', normalize-space(@class), ' '), ' btn ')]
When to ask for test ids
If you find yourself writing starts-with and following:: chains for the same component in several tests, that is the signal to ask the front-end team for a data-testid. One attribute replaces a dozen fragile locators, and most frameworks strip test ids from production bundles if that is a concern.
Try It Yourself
Open in Playground →
The Dynamic IDs sample uses generated ids and classes. Click elements to see which suggestions are rated stable.
Next Steps
- Anchoring to Stable Context - Walking from stable landmarks
- Forms and Labels - Finding inputs through their labels
- Locator Strategy in 2026 - Where XPath fits alongside role and test-id locators