Element Refs (@N) and Locators in Ego-Browser Snapshots: The Dual Addressing System
Element refs (@N) are stable numeric identifiers mapped to DOM backend nodes, while locators are human-readable selectors (CSS, XPath, ARIA) that resolve on-the-fly, together providing a resilient dual-mode addressing system for browser automation.
Ego-browser generates a semantic snapshot of the current page each time a script runs. According to the citrolabs/ego-lite source code, this snapshot captures two distinct addressing mechanisms that allow agents to locate elements: numeric refs that persist across DOM updates, and descriptive locators that survive UI changes.
Understanding Element Refs (@N)
Element refs are short-hand numeric identifiers generated during snapshot creation. In src/ref-map.ts, the runtime builds a RefMap that stores each element's mapping between a numeric ID and its Chrome DevTools Protocol (CDP) backendNodeId.
The parseRef function (src/ref-map.ts#L43-L55) recognizes three input formats:
// src/ref-map.ts#L43-L55
export function parseRef(input) {
const trimmed = String(input || "").trim();
for (const candidate of [
trimmed.startsWith("@") ? trimmed.slice(1) : null,
trimmed.startsWith("ref=") ? trimmed.slice(4) : null,
trimmed,
]) {
if (candidate && /^\d+$/.test(candidate)) {
return candidate;
}
}
return null;
}
When a snapshot is created via snapshotRaw (src/driver/observe.ts), the system populates the map using RefMap.addWithFrame (src/ref-map.ts#L8-L28):
// src/ref-map.ts#L8-L28
addWithFrame(refId, backendNodeId, role, name, nth = undefined, frameId = undefined) {
this.map.set(refId, { backendNodeId, role, name, nth, selector: undefined, frameId });
}
To resolve a ref, resolveElementCenter (src/element-resolver.ts#L70-L120) extracts the numeric ID, retrieves the stored backendNodeId, and calls DOM.getBoxModel. If the node has become stale, it falls back to an accessibility tree search using the stored role and name metadata.
Understanding Locators
Locators are declarative strings describing how to find elements using standard or accessibility-based selectors. Unlike refs, locators are not stored in the snapshot but are parsed on-demand by parseLocator (src/element-resolver.ts#L140-L186).
The resolver recognizes multiple prefixes:
css:– Standard CSS selectorsxpath:– XPath expressionstext:– Visible text contentlabel:,placeholder:,alt:,title:– Attribute-based selectorstestid:– Data-testid attributeshref:– URL path matchingrole:– ARIA role with optional name attribute (e.g.,role:button[name="Submit"])
When resolving locators, the system generates JavaScript code via buildLocatorCenterJs and buildLocatorFindJs (src/element-resolver.ts#L78-L95), which execute inside the page context to compute element coordinates. The queryAllExpression function in src/locator-query.ts#L21-L79 handles the translation of these selectors into executable DOM queries.
The Dual Resolution Strategy
The ego-browser implements a hierarchical resolution strategy that combines the speed of refs with the resilience of locators.
Refs are stable across snapshots. When the DOM updates or navigates occur, snapshotRaw refreshes the RefMap automatically, updating backendNodeId associations while preserving the numeric identifiers. This allows agents to reuse @N references without re-querying the DOM.
Locators are stable across releases. Because they rely on semantic attributes (ARIA roles, text content, CSS classes) rather than generated IDs, locators survive code changes and work across different page versions.
When resolving an element, resolveElementCenter first attempts the ref lookup. If the ref is stale (e.g., after navigation), the resolver automatically falls back to the locator, throwing ElementResolutionError with a "transient" kind to signal retry-ability.
Practical Implementation
To capture a snapshot and access the refs map:
// src/helpers.ts#L73-L75
const snap = await page.snapshotRaw();
console.log(snap.refs); // { "42": { backendNodeId: 12345, role: "button", name: "Submit" } }
Interacting with elements using both addressing modes:
// Fast lookup using ref (no DOM traversal)
await page.click("@42");
// Semantic locator (resilient to DOM changes)
await page.click('role:button[name="Confirm"]');
await page.click('css:button.submit');
await page.click('text=Login');
await page.click('href:/settings/profile');
// Internal nth selector for specific instances
await page.click('internal:nth=2;loc=css:.item');
The helper methods in src/helpers.ts (such as click and waitForSelector) ultimately delegate to resolveElementCenter or resolveElementObjectId, which handle the dual-mode fallback logic automatically.
Core Source Files
Understanding these files is essential for working with the addressing system:
src/ref-map.ts– Implements theRefMapclass for storing@Nmappings and theparseRefutility.src/element-resolver.ts– ContainsresolveElementCenter,resolveElementObjectId, andparseLocatorfor coordinating ref and locator resolution.src/locator-query.ts– ProvidesqueryAllExpressionand related helpers for translating locator strings into JavaScript DOM queries.src/helpers.ts– Exposes the public API (click,snapshot, etc.) that agents use to interact with the page.src/driver/observe.ts– ImplementssnapshotRawandsnapshot, the entry points for generating the semantic ref map.
Summary
- Element refs (
@N) provide fast, numeric addressing tied tobackendNodeIdvalues that persist across DOM updates via automatic map refreshing. - Locators offer human-readable, semantic addressing using CSS, XPath, ARIA roles, and text matching that survives code changes.
- Dual resolution attempts refs first for performance, falling back to locators automatically when refs become stale, with clear transient error semantics.
- Implementation spans
src/ref-map.tsfor storage,src/element-resolver.tsfor resolution logic, andsrc/locator-query.tsfor selector execution.
Frequently Asked Questions
What makes @N refs stable across page updates?
The RefMap automatically updates after every navigation or DOM mutation when snapshotRaw runs. While the backendNodeId may change internally, the numeric reference ID (the @N value) remains constant in the agent's context, allowing scripts to reuse the same identifier throughout a session.
Which locator syntaxes does ego-browser support?
As implemented in src/element-resolver.ts#L140-L186, the system supports css:, xpath:, text:, label:, placeholder:, alt:, title:, testid:, href:, and role: prefixes. It also handles internal selectors like internal:nth=... for indexing and internal:scope: for scoping.
How does the fallback mechanism work when a ref becomes stale?
When resolveElementCenter encounters a stale ref (missing or invalid backendNodeId), it throws ElementResolutionError with the kind set to "transient". The agent can catch this error and retry resolution using the associated locator data (role/name metadata or explicit locator strings), which queries the current DOM state rather than relying on cached node IDs.
When should I use refs versus locators in automation scripts?
Use refs (@N) for repeated interactions within the same page state where speed matters, as they avoid DOM traversal. Use locators when writing maintainable, cross-version scripts that must survive UI refactoring, or when addressing elements across different pages where numeric IDs would not be consistent.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →