Using Dynamic Selectors
Configure Axe Watcher to properly track accessibility issues on pages with dynamically generated IDs, classes, and URLs
When testing pages that generate dynamic element IDs or class names on each page load, Axe Watcher may have difficulty tracking whether accessibility issues are duplicates across test runs. This article explains how to configure Watcher to handle these scenarios correctly. A dynamic URL causes the same symptom for a different reason and needs a different fix, covered in Dynamic URLs below.
The Problem with Dynamic Selectors
By default, Axe Watcher uses CSS selectors that include element IDs and classes to identify where accessibility issues occur. For example, an issue might be reported at:
iframe#main-iframeThis approach works well when your page's IDs and classes remain consistent between page loads. However, many modern web applications generate dynamic identifiers that change on every page render, such as:
#component-a1b2c3d4.form-field-xyz789
Axe Watcher also sends an XPath for each element, and Axe Developer Hub treats a match on either selector as the same element. Because an XPath never includes class names, a page whose class names alone are dynamic is often tracked correctly without any further configuration. An XPath does include an element's ID when that ID is unique on the page, though, producing a path such as //div[@id='component-a1b2c3d4']. A dynamic ID therefore changes the CSS selector and the XPath together, leaving nothing stable to match on.
When identifiers change between test runs this way, Axe Watcher cannot determine whether an issue is a duplicate of a previously detected issue or a new occurrence. This can result in:
- The same issue being reported as both "new" and "resolved" on each test run
- Inaccurate tracking of your accessibility progress over time
- Difficulty identifying which issues have genuinely been fixed
The Solution: Enable Ancestry Tracking
To handle dynamic selectors, set the ancestry option to true in your runOptions configuration. When enabled, Axe Watcher uses the element's position within the DOM tree rather than relying on IDs and classes to locate elements between test runs.
With ancestry enabled, a selector that previously looked like this:
iframe#main-iframeWill instead include the complete path from the root element:
html > body > div:nth-child(20) > div:nth-child(1) > div > div > ul > li:nth-child(1) > div > span > iframeThis positional selector remains consistent across page loads, even when IDs and classes change, allowing Axe Watcher to accurately track duplicate issues.
Configuration Examples
JavaScript and TypeScript
Add the ancestry option to your axe configuration's runOptions:
const config = {
axe: {
apiKey: process.env.ACCESSIBILITY_API_KEY,
projectId: process.env.PROJECT_ID,
runOptions: {
ancestry: true
}
}
}Java
Use the setAncestry() method on your AxeRunOptions object:
AxeRunOptions runOptions = new AxeRunOptions()
.setAncestry(true);
AxeWatcherOptions options = new AxeWatcherOptions()
.setApiKey(System.getenv("ACCESSIBILITY_API_KEY"))
.setProjectId(System.getenv("PROJECT_ID"))
.setRunOptions(runOptions);
AxeWatcher watcher = new AxeWatcher(options);When to Use Ancestry Tracking
Enable ancestry: true when your application:
- Uses frameworks that generate dynamic component IDs (React, Vue, Angular)
- Employs CSS-in-JS libraries that generate unique class names
- Has form fields or interactive elements with auto-generated identifiers
- Shows inconsistent "new issue" and "resolved issue" counts between test runs for what appear to be the same issues
Trade-offs to Consider
While ancestry tracking solves the dynamic selector problem, there are some considerations:
- Selector readability: Positional selectors are longer and can be harder to read when reviewing issues in Axe Developer Hub.
- DOM structure sensitivity: If your page's DOM structure changes significantly between renders (not just the IDs/classes), the positional selectors may also change.
- Debugging: When investigating an issue, you may find it easier to locate an element by a meaningful ID than by its position in the DOM tree.
For most applications with dynamic identifiers, the benefits of accurate issue tracking outweigh these trade-offs.
Dynamic URLs
Element selectors are only part of how Axe Developer Hub identifies an issue. The URL of the page is compared as well, in full, including any query string. Enabling ancestry has no effect on this comparison.
A URL that varies between test runs therefore produces the same symptoms as a dynamic selector, and does so more aggressively: when a URL hasn't been seen before, every issue found on it is reported as new, without the element selectors being consulted at all. Common causes include:
- Session IDs or authentication tokens carried in the query string
- Timestamps or cache-busting parameters, such as
?v=1738012800 - Randomized IDs in the path, such as
/orders/8f3c1b2a/summary - Test fixtures that generate a fresh record, and therefore a fresh URL, on each run
There is no configuration option that normalizes URLs before they are compared, so this has to be solved in your test suite: navigate to deterministic URLs, so that the same page produces the same URL on every run. In practice this usually means seeding fixed test data instead of generating new records per run, and removing or pinning volatile query parameters before the page is scanned.
If a URL genuinely cannot be stabilized, you can keep it out of your results entirely with excludeUrlPatterns, though this means the page isn't scanned at all rather than tracked correctly. See Exclude URLs from Analysis for details.
See Also
- API Reference for JavaScript and TypeScript Complete documentation for
runOptionsand other configuration options AxeRunOptionsClass Java API reference for runtime options- Glossary: Duplicate Understanding how Axe Developer Hub identifies duplicate issues
