Q23 of 37 · Selenium
How do you handle dynamic web elements with changing IDs?
Short answer
Short answer: Use stable attributes the framework doesn't generate — `data-test`, role, name, or text — instead of generated ids. If you must match a generated id, use partial matches (`[id^=user-row-]`, `contains(@id,'row-')`) anchored on the stable prefix.
Detail
Dynamic ids are most often a side-effect of UI frameworks: React's useId, MUI's mui-12345, Angular's cdk-overlay-2. The id changes between renders or releases — locators that hard-code it break.
The right answer is to stop relying on the id:
Ask devs for
data-testattributes. The single best move.[data-test=user-row-42]beats every other locator strategy and is the de-facto industry standard.Use stable user-facing attributes. Forms have
name. Buttons have visible text. Roles (role=dialog,role=button) are stable across visual refactors.Use scoped, structural CSS:
By.cssSelector("[data-section=users] tr td:nth-child(2)")
When you can't get devs to add hooks:
// Starts-with — anchor on the stable prefix
By.cssSelector("[id^='user-row-']")
// Contains — partial match anywhere in the id
By.xpath("//div[contains(@id,'row-')]")
// Matches — full regex (xpath 2.0 only, niche)
By.xpath("//div[matches(@id, 'row-\\d+')]")
Anti-patterns to avoid:
- Indexed selectors (
(//tr)[3]) — break the moment a row is added. - Generated class names from CSS-in-JS (
.css-1q2w3e4) — change every build. getAttribute("id")then string-matching — pushes fragility from locator into code.
A cultural note: when you find yourself writing contains(@id, ...) patterns repeatedly, that's a signal the app needs test hooks. Push back rather than building elaborate workarounds.
// EXAMPLE
// ❌ Fragile — generated id changes each render
By.id("user-row-mui-12345")
// ✅ Anchor on the stable prefix
By.cssSelector("[id^='user-row-']")
// ✅ Better — use the framework-agnostic test hook
By.cssSelector("[data-test='user-row-42']")
// ✅ When CSS isn't enough — relative xpath via attribute,
// not position
By.xpath("//div[contains(@class,'user-row') and @data-id='42']")