What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Do not hard-code the changing part of an element’s id. Match the stable portion with a CSS attribute selector such as input[id^="user_"] (prefix), button[id$="_submit"] (suffix), or [id*="checkout"] (substring). Narrow that selector with a tag, stable ancestor, or semantic attribute, then use a Puppeteer locator or an explicit waitForSelector before interacting.
The selector decision order
A dynamic ID is usually generated from a random token, session value, or component instance. The complete value changes, but part of it often remains meaningful. Choose selectors in this order:
- Use a stable semantic hook first. Prefer an accessible role and name, visible text, a label,
data-testid, or another documented attribute that expresses the element’s purpose. - Match the stable ID fragment if no better hook exists. CSS attribute selectors can match a prefix, suffix, or substring without knowing the volatile characters.
- Make the match unique. Add an element type, stable container, or another attribute.
- Synchronize before acting. A locator is the recommended interaction API; use
waitForSelectorwhen you need explicit synchronization or lower-level control. - Use prefixed XPath only when CSS cannot express the condition.
| Need | Selector | Meaning |
|---|---|---|
| Known beginning | [id^="prefix"] |
ID starts with prefix |
| Known ending | [id$="suffix"] |
ID ends with suffix |
| Known fragment | [id*="fragment"] |
ID contains fragment |
| Scoped match | #stable-panel input[id*="email"] |
Only matching inputs inside a stable panel |
CSS patterns for changing IDs
Prefix matching
Use ^= when the application keeps a fixed beginning and appends a changing suffix:
const save = page.locator('button[id^="save-"]');
await save.click();
This matches IDs such as save-1842 and save-a91f, but not an unrelated button whose ID starts differently.
#1 Best Overall
Suffix matching
Use $= when the stable portion is at the end:
const submit = 'form button[id$="-submit"]';
await page.waitForSelector(submit, {visible: true});
await page.click(submit);
The tag and form scope reduce accidental matches when several components share the same suffix.
Substring matching
Use *= when the stable text can occur anywhere in the ID:
const field = page.locator('#settings-panel input[id*="email"]');
await field.fill('[email protected]');
Substring matching is the broadest option. If the fragment is common, add a stable ancestor, tag, type, or another attribute.
Combining attributes
CSS selectors can express several constraints:
const primary = page.locator(
'section[data-testid="checkout"] button[type="submit"][id^="pay-"]'
);
await primary.click();
Use quotes around attribute values, and escape any characters that are not valid in a CSS identifier. An attribute selector avoids needing to escape a random ID suffix.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minutePrefer semantic and test selectors when available
An ID-pattern selector is a fallback, not automatically the best selector. A role, label, visible text, or test hook communicates intent and usually survives a redesign better than an implementation-generated ID.
Rank #2
Role and accessible name
const checkout = page.getByRole('button', {name: 'Checkout'});
await checkout.click();
Use this when the role and accessible name are stable and unique. It also tests the interface in terms closer to what an assistive-technology user perceives.
Label and test hook
await page.getByLabel('Email address').fill('[email protected]');
await page.locator('[data-testid="account-email"]').fill('[email protected]');
Only adopt a data-testid contract that the application team intends to keep stable. If the page offers no such contract, ask for one rather than building a selector around presentation-only classes.
Text selectors
Visible text can be useful for a unique action, but it may change with localization or copy edits. Scope it to a stable region when possible.
Free tools Windows power users keep installed
One-click scans. No signup required.
Locators versus explicit waits
Locators for normal interactions
Puppeteer documentation describes locators as the recommended way to select and interact with elements. A locator waits for the element to be present and in the required state, retries the operation when needed, and makes a dynamic page less timing-sensitive:
const save = page.locator('button[id^="save-"]');
await save.click();
Use locator methods such as click() and fill() for actions that should wait for readiness.
waitForSelector for explicit synchronization
Use page.waitForSelector when you need a handle, visibility control, a custom timeout, or an abort signal. It waits for a selector to appear, works across navigations, and supports visible, hidden, timeout, and signal. Its documented default timeout is 30 seconds.
const selector = 'form button[id$="-submit"]';
await page.waitForSelector(selector, {
visible: true,
timeout: 15000
});
await page.click(selector);
A wait only proves that a matching node reached the requested state. It does not prove that the selector is unique or that the button represents the correct form, so still scope and inspect matches.
Check uniqueness before clicking
Dynamic fragments can match more than one element. Inspect the result during development or fail deliberately when the count is not what you expect.
const matches = await page.$$('input[id^="user-"]');
console.log('matched elements:', matches.length);
if (matches.length !== 1) {
throw new Error(`Expected one user input, found ${matches.length}`);
}
await matches[0].type('alice');
page.$ returns the first matching element; page.$$ returns all matches. Selecting the first match without checking can silently operate on the wrong component.
Read values with $eval and $$eval
$eval passes one matched element to a page function and throws if none matches. $$eval passes an array of all matches and waits for an asynchronous page function:
Rank #4
const value = await page.$eval(
'input[id$="-email"]',
el => el.value
);
const ids = await page.$$eval(
'input[id*="email"]',
elements => elements.map(el => el.id)
);
Use these APIs for inspection or extraction; use a locator for an interaction that needs readiness and retry behavior.
XPath when CSS is insufficient
CSS cannot express every condition. Puppeteer supports XPath through its prefixed syntax, which uses the browser’s native Document.evaluate:
const button = await page.waitForSelector(
'::-p-xpath(//button[starts-with(@id,"save-")])'
);
await button.click();
Keep the XPath condition specific. If an XPath expression can be replaced by a stable role, label, test hook, or CSS selector, the simpler selector is generally easier to maintain.
Dynamic rendering and navigation patterns
Wait after navigation
await page.goto('https://example.com/account', {
waitUntil: 'domcontentloaded'
});
await page.locator('form button[id$="-submit"]').click();
For a single-page application, navigation may finish before the component renders. Wait for the stable selector, not an arbitrary sleep:
await page.goto('https://example.com/app');
await page.waitForSelector('#settings-panel input[id*="email"]', {
visible: true
});
Handle a selector that is intentionally optional
const dismiss = 'button[id^="newsletter-dismiss-"]';
const close = await page.$(dismiss);
if (close) {
await close.click();
}
Use an explicit, short timeout when an optional element should not hold the test for the full default timeout.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Best Value
- Used Book in Good Condition
Shadow DOM
Puppeteer supports custom selector syntax for shadow DOM as well as CSS, XPath, text, and accessibility selectors. If the dynamic-ID element is inside a web component, use Puppeteer’s shadow-DOM selector facilities or a locator that crosses the component boundary rather than assuming a document-level CSS query can reach it.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Common failures and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Timeout waiting for selector | Prefix, suffix, or scope is wrong; element has not rendered; or navigation is still in progress. | Inspect the live DOM, verify the stable fragment, wait after the relevant navigation, and increase the timeout only after fixing synchronization. |
| More than one element matches | The fragment is shared by repeated rows, hidden templates, or multiple panels. | Add a stable ancestor, tag, role, label, or test attribute; verify with page.$$. |
| Click targets a hidden or covered node | A template copy, overlay, or consent dialog matches first. | Use a locator, require visibility, scope to the active panel, and handle the overlay explicitly. |
| Selector works locally but not in CI | Different timing, viewport, authentication state, or feature flag changes the DOM. | Wait on a meaningful state, set required cookies or headers, and capture diagnostics such as matching IDs and a screenshot. |
| Special characters break the selector | The fragment contains CSS-significant characters. | Use a quoted attribute selector with proper escaping, or choose a semantic hook/XPath. |
| Element is inside a component | It lives in shadow DOM or an iframe. | Use Puppeteer’s shadow-DOM syntax; for an iframe, obtain its frame and query within that frame. |
Reliability and maintenance checklist
- Document which part of the ID is guaranteed stable and who owns that contract.
- Prefer one unique semantic or test selector over a long chain of incidental classes.
- Scope repeated patterns to a stable container.
- Assert the expected match count during development and in critical tests.
- Wait for visible, enabled, or otherwise actionable state instead of sleeping for a fixed duration.
- Log the selector, URL, and matched IDs when a test fails.
- Re-check selectors after component-library, localization, and accessibility changes.
Or skip the browser setup
If your goal is a rendered page image rather than an interaction test, ScreenshotNeo returns a screenshot or PDF from one request. It accepts cookie and consent banners as a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are free, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
Use the API documentation at https://screenshotneo.com/docs/ for all options, including full-page lazy-image capture, CSS-selector element capture, dark mode, device presets, retina scale, PDF paper settings, custom CSS and JavaScript, clicks, selector/delay/network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage data, and the OpenAPI specification.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is included on every plan. Create a free ScreenshotNeo account.
FAQ
Can I use a regular ID selector with a changing suffix?
Only if you know the complete value at runtime. Otherwise use an attribute selector such as [id^="prefix"], [id$="suffix"], or [id*="fragment"].
Should I use page.$ or a locator?
Use a locator for normal actions that need readiness and retries. Use page.$ when you specifically need the first matching element handle, and verify that first-match behavior is intentional.
What if the changing ID has no stable portion?
Ask for or identify another stable contract: role and accessible name, label, visible text, a test attribute, a stable ancestor, or a documented application state.
Frequently Asked Questions
Does Puppeteer support CSS prefix, suffix, and substring selectors?
Yes. Puppeteer accepts standard CSS attribute selectors: ^= for prefixes, $= for suffixes, and *= for substrings.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallWhat is the default waitForSelector timeout?
The documented default is 30 seconds; you can override it with the timeout option.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




