Blog

How to Handle Dynamic Elements in Selenium

Rishabh Kumar
Marketing Lead
Published on
April 28, 2026
In this Article:

Handle dynamic elements in Selenium with proven strategies for dynamic IDs, AJAX content, changing attributes, and AI alternatives that eliminate the problem.

Modern web applications generate element attributes dynamically. IDs change with each page load. Classes include randomly generated hashes. Content loads asynchronously. Lists grow and shrink based on user actions. These dynamic behaviors break traditional Selenium locators that expect static, predictable elements. This guide covers every type of dynamic element challenge and provides working solutions for each, from immediate workarounds to architectural approaches that handle dynamics automatically.

Understanding Dynamic Elements

What Makes Elements Dynamic

Dynamic elements have attributes or behaviors that change without explicit user action or between sessions:

Dynamic IDs:

The same form field generates a completely different ID on every page load, making any locator that targets it by ID unreliable from the start.

<!-- First page load -->
<input id="email_field_a7f3e2" type="email">

<!-- Second page load -->
<input id="email_field_k9x2m1" type="email">

Dynamic Classes:

CSS-in-JS libraries like Styled Components and CSS Modules generate hashed class names at build time, meaning the class you recorded in your test will not exist in the next build.

<!-- Generated by CSS-in-JS or CSS Modules -->
<button class="Button_primary__3xK2p">Submit</button>

Dynamic Content:

Elements that depend on an API response are present in the DOM but empty until the data arrives, causing tests that interact with them too early to fail or return incorrect results.

<!-- Content loaded via AJAX -->
<div id="results">
  <!-- Empty until API responds -->
</div>

Dynamic Lists:

Lists driven by real-time data have no fixed length or content, so any test that references a specific item by position or count will break as soon as the underlying data changes.

<!-- Items change based on filters, searches, or real-time updates -->
<ul id="notifications">
  <!-- 0 to N items, constantly changing -->
</ul>

Why Applications Use Dynamic Elements

Dynamic attributes are not bugs. They serve specific engineering purposes and understanding them helps you choose the right handling strategy rather than fighting the framework.

  • Unique IDs: Frameworks generate unique IDs to prevent conflicts in component based architectures
  • Scoped CSS: CSS Modules and CSS-in-JS add hashes to prevent style collisions
  • Security: Some applications randomize IDs to prevent scraping or manipulation
  • State Reflection: Classes change to reflect current state (active, disabled, selected)

The Challenge for Test Automation

Static locators work fine when attribute values never change. The moment an application starts generating attributes dynamically, every locator strategy that depends on those values becomes a maintenance liability.

Here is what that looks like in practice. Both of these locators will fail the next time the page loads with freshly generated values.

# Fails when ID changes
driver.find_element(By.ID, "email_field_a7f3e2")

# Fails when class hash changes  
driver.find_element(By.CLASS_NAME, "Button_primary__3xK2p")

Every locator strategy that depends on dynamic values eventually breaks. The question is not whether it will fail but when.

Strategies for Dynamic IDs and Classes

None of these strategies eliminates the underlying problem of dynamic attributes. What they do is make your locators resilient enough to survive the specific type of change you are dealing with. The right choice depends on what part of the attribute stays stable.

Strategy 1: Use Partial Attribute Matching

When an ID or class always contains a predictable substring even if the rest changes, you can target the stable portion instead of the full value. XPath and CSS both support partial string matching with slightly different syntax.

XPath Contains:

When an ID or class contains a stable prefix followed by a generated hash, targeting the predictable portion of the string keeps your locator working even as the dynamic suffix changes.

# Matches any ID containing "email_field"
driver.find_element(By.XPATH, "//*[contains(@id, 'email_field')]")

# Matches any class containing "Button_primary"
driver.find_element(By.XPATH, "//*[contains(@class, 'Button_primary')]")

XPath Starts-With:

If the dynamic portion always appears at the end of an attribute value, anchoring your selector to the beginning of the string is a more precise match than a general contains check.

# Matches IDs starting with "email_field_"
driver.find_element(By.XPATH, "//*[starts-with(@id, 'email_field_')]")

CSS Partial Matching:

CSS attribute selectors offer the same partial matching capability as XPath but with a more concise syntax. Choose the operator based on where the stable portion of the attribute value sits: anywhere in the string, at the start, or at the end.

# Attribute contains value
driver.find_element(By.CSS_SELECTOR, "[id*='email_field']")

# Attribute starts with value
driver.find_element(By.CSS_SELECTOR, "[id^='email_field']")

# Attribute ends with value
driver.find_element(By.CSS_SELECTOR, "[id$='_field']")

Strategy 2: Use Stable Alternative Attributes

When IDs and classes are fully dynamic, shift your locator to attributes that developers do not regenerate. These attributes are more likely to remain stable across page loads and application updates because they serve functional or accessibility purposes rather than styling.

Name Attribute:

Form fields almost always carry a name attribute that maps to the field's purpose and rarely changes between releases.

driver.find_element(By.NAME, "email")

Data Attributes:

Data attributes are specifically intended for non-presentational metadata. The data-testid convention exists precisely to give automation a stable hook that is independent of styling and functionality.

# data-testid is commonly added for testing
driver.find_element(By.CSS_SELECTOR, "[data-testid='email-input']")

# Other data attributes
driver.find_element(By.CSS_SELECTOR, "[data-field='email']")

ARIA Attributes:

Accessibility attributes describe what an element does rather than how it looks, making them among the most stable locators available. They change only when the element's purpose changes.

driver.find_element(By.CSS_SELECTOR, "[aria-label='Email address']")
driver.find_element(By.CSS_SELECTOR, "[role='textbox'][aria-describedby='email-help']")

Form Attributes:

Input type and placeholder attributes are set by developers to communicate purpose to users and browsers. They tend to remain stable because changing them changes the user experience.

driver.find_element(By.CSS_SELECTOR, "input[type='email']")
driver.find_element(By.CSS_SELECTOR, "input[placeholder='Enter email']")

Strategy 3: Use Text Content

Visible text is often the most stable thing on a page. Button labels, link text, and headings change when the product changes its terminology, which happens far less frequently than implementation details like class names and IDs.

Link Text:

Selenium's link text strategies match anchor elements by their visible label, which maps directly to what a user would read and click.

driver.find_element(By.LINK_TEXT, "Sign In")
driver.find_element(By.PARTIAL_LINK_TEXT, "Sign")

XPath Text Matching:

XPath text functions give you flexible control over how strictly you match visible text, from exact matches to contains checks with whitespace normalisation for text that wraps or has inconsistent spacing.

# Exact text match
driver.find_element(By.XPATH, "//button[text()='Submit']")

# Contains text
driver.find_element(By.XPATH, "//button[contains(text(), 'Submit')]")

# Normalize whitespace
driver.find_element(By.XPATH, "//button[normalize-space()='Submit Order']")

Strategy 4: Use Relative Positioning

When an element has no stable attributes of its own, locate it in relation to a nearby element that does. This strategy works particularly well for form fields associated with labels, buttons inside specific containers, and elements within well-defined page regions.

XPath Axes:

XPath axis selectors let you navigate the DOM tree directionally from a stable anchor point, moving to siblings, descendants, or parents of known elements.

# Find input following a specific label
driver.find_element(By.XPATH, "//label[text()='Email']//following-sibling::input")

# Find button inside a specific form
driver.find_element(By.XPATH, "//form[@id='login']//button[@type='submit']")

# Find element by parent context
driver.find_element(By.XPATH, "//div[@class='checkout-section']//input[@type='text']")

CSS Combinators:

CSS combinators express structural relationships between elements more concisely than XPath axes. Use them when the relationship between your anchor and target is straightforward.

# Direct child
driver.find_element(By.CSS_SELECTOR, ".form-group > input")

# Descendant
driver.find_element(By.CSS_SELECTOR, "#login-form input[type='email']")

# Adjacent sibling
driver.find_element(By.CSS_SELECTOR, "label + input")

Strategy 5: Multiple Attribute Combination

When a single attribute match is too broad and returns multiple elements, combining multiple conditions narrows the result to a unique match. This is the most precise manual locator strategy and the most robust against unintended matches.

Combining conditions reduces the chance that a partial match accidentally targets the wrong element when similar elements exist elsewhere on the page.

# Element with class containing 'btn' AND 'primary'
driver.find_element(By.XPATH, "//*[contains(@class, 'btn') and contains(@class, 'primary')]")

# Input with type email inside element with role form
driver.find_element(By.CSS_SELECTOR, "[role='form'] input[type='email']")

# Button containing 'Submit' text with type submit
driver.find_element(By.XPATH, "//button[@type='submit' and contains(text(), 'Submit')]")

CTA Banner

Handling AJAX and Asynchronously Loaded Elements

AJAX content introduces a timing dimension that locator strategies alone cannot solve. The element may be perfectly identifiable once it exists, but if your test looks for it before the API responds, it simply will not be there.

The Timing Challenge

The page load event fires when the initial HTML is parsed. AJAX requests fire after that. Any element that depends on an API response is absent from the DOM during the window between page load and API response completion.

Here is what that failure looks like. The page loads, the test immediately looks for the element, and the element does not exist yet because the API has not responded.

driver.get("https://example.com/dashboard")
# Content loads via AJAX after page load
driver.find_element(By.ID, "user-data")  # NoSuchElementException

Explicit Waits for Element Presence

Rather than assuming the element exists, tell Selenium to wait until it appears. Explicit waits poll the DOM repeatedly until the condition is met or the timeout expires, giving AJAX time to complete without a fixed delay.

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# Wait up to 10 seconds for element to be present
element = WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.ID, "user-data"))
)

Wait for Element Visibility

An element being present in the DOM does not mean a user can see or interact with it. Some elements load hidden and become visible only after additional processing. Visibility waits check both conditions simultaneously.

# Wait for element to be visible (present AND displayed)
element = WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.ID, "user-data"))
)

Wait for Element to Be Clickable

Interactive elements like buttons and links may be present and visible but temporarily disabled while a form validates or a request processes. A clickability wait ensures the element is both visible and enabled before your test attempts to interact with it.

# Wait for element to be clickable (visible AND enabled)
button = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.ID, "submit-button"))
)
button.click()

Wait for Text Content

Some elements appear in the DOM immediately as empty containers and fill with content when data arrives. Waiting for the element itself is not enough in these cases. You need to wait for the content inside it.

# Wait for element to contain expected text
WebDriverWait(driver, 10).until(
    EC.text_to_be_present_in_element((By.ID, "status"), "Complete")
)

# Wait for any text to appear
def element_has_text(locator):
    def check(driver):
        element = driver.find_element(*locator)
        return element.text.strip() != ""
    return check

WebDriverWait(driver, 10).until(element_has_text((By.ID, "results")))

Wait for Loading Indicators to Disappear

Applications that show a spinner or loading overlay during AJAX calls give you an indirect signal you can wait on. When the spinner disappears, the data it was waiting for has arrived and the content is ready to interact with.

# Wait for spinner to disappear
WebDriverWait(driver, 10).until(
    EC.invisibility_of_element_located((By.CLASS_NAME, "loading-spinner"))
)

# Then interact with loaded content
driver.find_element(By.ID, "results").click()

Custom Wait Conditions

Standard expected conditions cover common scenarios but not application-specific ones. When your application uses a known framework like jQuery or Angular, or has a detectable network activity pattern, you can write a wait condition that checks exactly the signal that matters for your application.

These custom conditions give you more reliable synchronisation than generic waits because they check the application's own readiness signals rather than inferring readiness from element state.

def ajax_complete(driver):
    """Wait for all AJAX requests to complete"""
    return driver.execute_script("return jQuery.active == 0")

def angular_ready(driver):
    """Wait for Angular to finish processing"""
    return driver.execute_script(
        "return window.getAllAngularTestabilities().every(t => t.isStable())"
    )

def network_idle(driver):
    """Wait for network activity to settle"""
    return driver.execute_script("""
        return performance.getEntriesByType('resource')
            .filter(r => r.responseEnd === 0).length === 0
    """)

# Usage
WebDriverWait(driver, 10).until(ajax_complete)

Handling Dynamic Lists and Tables

Lists and tables present a distinct challenge from dynamic attributes: the structure itself changes. Rows appear and disappear, item counts shift, and positions move based on filters, sort orders, and real-time updates.

Lists That Change Size

When list length is unpredictable, waiting for a specific item count before interacting prevents your test from running against an incomplete list. This is particularly important for search results and filtered lists where the count depends on query results.

# Wait for list to have items
def list_has_items(locator, min_count=1):
    def check(driver):
        items = driver.find_elements(*locator)
        return len(items) >= min_count
    return check

WebDriverWait(driver, 10).until(
    list_has_items((By.CSS_SELECTOR, ".search-result"), 5)
)

Finding Items by Content

Referencing list items by their content rather than their position makes your test independent of the current sort order and page state. As long as the item with that content exists in the list, the locator works regardless of where it appears.

# Find row containing specific text
driver.find_element(By.XPATH, "//tr[contains(., 'Order #12345')]")

# Find list item by data
driver.find_element(By.XPATH, "//li[@data-product-id='SKU123']")

# Find card by heading text
driver.find_element(By.XPATH, "//div[contains(@class, 'card')]//h3[text()='Premium Plan']/..")

Working with Indices Carefully

Position-based selection is the most fragile approach to dynamic lists and should be avoided where content-based alternatives exist. When index-based selection is genuinely necessary, always verify the list length first to avoid silent failures from out-of-bounds access.

# Get all items first
items = driver.find_elements(By.CSS_SELECTOR, ".list-item")

# Verify expected count before indexing
if len(items) >= 3:
    items[2].click()
else:
    raise Exception(f"Expected at least 3 items, found {len(items)}")

When you must use an index, narrow the set first by filtering to elements with specific attributes so the index refers to a meaningful position within a defined subset rather than an arbitrary position in the full list.

# Find the third item with specific class
items = driver.find_elements(By.CSS_SELECTOR, ".list-item.active")
# Now index refers to "third active item" not "third item"

Tables with Dynamic Rows

Tables are the most complex dynamic structure because they combine variable row counts with the need to cross-reference row content against column headers. Hard-coding column indices breaks when columns are reordered. This function resolves the column index from the header text at runtime, making it resilient to column reordering.

# Find cell in row containing "John Smith", column "Email"
def get_cell_by_row_content_and_header(driver, row_content, header_text):
    # Find column index
    headers = driver.find_elements(By.CSS_SELECTOR, "table th")
    col_index = None
    for i, header in enumerate(headers):
        if header.text == header_text:
            col_index = i + 1  # XPath is 1-indexed
            break
    
    if col_index is None:
        raise Exception(f"Header '{header_text}' not found")
    
    # Find row and cell
    return driver.find_element(
        By.XPATH, 
        f"//tr[contains(., '{row_content}')]/td[{col_index}]"
    )

email_cell = get_cell_by_row_content_and_header(driver, "John Smith", "Email")

Handling Elements That Change State

State changes introduce a temporal dimension where the same element is valid at some moments and invalid at others. Tests that do not account for state often produce false failures because they interact with elements at the wrong point in their lifecycle.

Elements That Enable/Disable

Buttons that are disabled until a form is complete, or until a previous action finishes, require a wait that checks the enabled state rather than just the element's presence.

# Wait for button to be enabled
WebDriverWait(driver, 10).until(
    lambda d: d.find_element(By.ID, "submit").is_enabled()
)

Elements That Show/Hide

Elements frequently toggle between visible and hidden states in response to user actions and application events. Both directions of this toggle need explicit handling: waiting for appearance before interacting and waiting for disappearance before asserting the element is gone.

# Wait for element to appear
WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.ID, "success-message"))
)

# Wait for element to disappear
WebDriverWait(driver, 10).until(
    EC.invisibility_of_element_located((By.ID, "error-message"))
)

Elements with Changing Classes

Class changes are a common pattern for reflecting UI state transitions such as a tab becoming active or a step becoming complete. Rather than polling the element repeatedly in your test code, a custom wait condition encapsulates this check cleanly.

def element_has_class(locator, class_name):
    def check(driver):
        element = driver.find_element(*locator)
        return class_name in element.get_attribute("class")
    return check

# Wait for tab to become active
WebDriverWait(driver, 10).until(
    element_has_class((By.ID, "settings-tab"), "active")
)

Virtuoso QA's AI Native Approach to Dynamic Elements

Why Traditional Strategies Accumulate Complexity

Each dynamic element strategy adds code that engineers must write, review, and maintain. The problem compounds over time.

  • Partial matches need careful pattern design to avoid unintended matches
  • Waits need custom conditions written for each specific application scenario
  • Relative locators require detailed knowledge of the current DOM structure
  • List handling requires defensive programming against unpredictable sizes

This complexity accumulates across the test suite. Test code becomes progressively harder to read, maintain, and debug as every new dynamic element adds another layer of handling logic.

Natural Language Eliminates Dynamic Element Challenges

AI-native test platform like Virtuoso QA identify elements the way humans do: by what they look like and what they do, not by the implementation details underneath them. When you describe an element in natural language, the platform handles the complexity of locating it across any combination of dynamic attributes.

Here is the same interaction written in traditional Selenium and in natural language. Both achieve the same result. One requires explicit handling of the dynamic ID. The other does not know the ID exists.

Traditional Selenium:

A test engineer must know that the email field has a dynamic ID, design a partial XPath match around the stable prefix, wrap it in an explicit wait, and maintain all of this if the prefix changes.

# Handle dynamic ID
element = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.XPATH, "//*[contains(@id, 'email_field')]"))
)
element.send_keys("user@example.com")

Natural Language:

The platform identifies the email field through label associations, placeholder text, input type, field position, and visual appearance simultaneously. The dynamic ID is irrelevant to the identification process.

Enter "user@example.com" in the email field

The platform identifies "email field" through multiple signals: label associations, placeholder text, input type, field position, visual appearance. Dynamic IDs are irrelevant.

Intelligent Element Identification

Virtuoso QA's AI Augmented Object Identification builds comprehensive element models:

Semantic Signals:

  • Associated labels and ARIA attributes
  • Placeholder text and field names
  • Element roles and types

Visual Signals:

  • Position on screen
  • Size and appearance
  • Relationship to other elements

Structural Signals:

  • Container context
  • Sibling relationships
  • Page region

When any single attribute changes, elements remain identifiable through other signals. Dynamic classes, IDs, and attributes do not cause failures.

Self Healing Handles Change

Beyond initial identification, self healing adapts when elements change:

  • ID hash regenerates: other attributes maintain identification
  • Class names update: text and position still match
  • DOM restructures: visual relationships persist

Virtuoso QA achieves approximately 95% self healing accuracy, automatically adapting to changes that would require manual locator updates in traditional frameworks.

CTA Banner

Related Reads

Frequently Asked Questions

Should I ask developers to add test IDs to elements?
Test IDs (data-testid attributes) help when developers cooperate consistently. However, this requires coordination, discipline, and coverage of every element tests might need. Many organizations cannot achieve this across complex applications with multiple teams. AI native approaches work regardless of test ID presence.
Which is better: XPath or CSS selectors for dynamic elements?
Both have strengths. CSS selectors are generally faster and more readable. XPath offers more power for complex conditions (text matching, axes navigation). For dynamic elements, the choice matters less than the strategy: both need partial matching, relative positioning, or alternative attributes to handle dynamics effectively.
Can I use JavaScript to find dynamic elements more reliably?
JavaScript execution can access elements Selenium struggles with, but it bypasses Selenium's synchronization and event simulation. Use JavaScript as a fallback, not a primary strategy. For consistently problematic elements, the issue is usually approach rather than tooling.
What if multiple elements match my partial locator?
Narrow your locator with additional criteria: combine partial class with element type, add parent context, or include text matching. If ambiguity persists, the page structure may need test IDs, or you need AI native identification that uses multiple signals to disambiguate automatically.
How do I test infinite scroll or lazy loaded content?
Trigger loading through scrolling, then wait for content to appear. Use custom wait conditions that verify expected items loaded. For AI native platforms, describe the element you need and the platform handles scrolling and waiting automatically.

How do I handle elements that take unpredictable time to load?

Use explicit waits with appropriate timeouts rather than fixed delays. Set timeouts based on worst case scenarios plus margin. For truly unpredictable timing, implement polling that checks conditions repeatedly. AI native platforms handle timing automatically through intelligent wait systems that observe actual application state.

Subscribe to our Newsletter

Codeless Test Automation

Try Virtuoso QA in Action

See how Virtuoso QA transforms plain English into fully executable tests within seconds.

Try Interactive Demo
Schedule a Demo