# Element Refs (@N) and Locators in Ego-Browser Snapshots: The Dual Addressing System

> Discover element refs @N and locators in ego-browser snapshots. Learn how this dual addressing system offers stable IDs and flexible selectors for robust automation.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: deep-dive
- Published: 2026-07-30

---

**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`](https://github.com/citrolabs/ego-lite/blob/main/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:

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts)), the system populates the map using `RefMap.addWithFrame` (`src/ref-map.ts#L8-L28`):

```typescript
// 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 selectors
- **`xpath:`** – XPath expressions
- **`text:`** – Visible text content
- **`label:`**, **`placeholder:`**, **`alt:`**, **`title:`** – Attribute-based selectors
- **`testid:`** – Data-testid attributes
- **`href:`** – URL path matching
- **`role:`** – 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:

```typescript
// 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:

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts)** – Implements the `RefMap` class for storing `@N` mappings and the `parseRef` utility.
- **[`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts)** – Contains `resolveElementCenter`, `resolveElementObjectId`, and `parseLocator` for coordinating ref and locator resolution.
- **[`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts)** – Provides `queryAllExpression` and related helpers for translating locator strings into JavaScript DOM queries.
- **[`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts)** – Exposes the public API (`click`, `snapshot`, etc.) that agents use to interact with the page.
- **[`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts)** – Implements `snapshotRaw` and `snapshot`, the entry points for generating the semantic ref map.

## Summary

- **Element refs (`@N`)** provide fast, numeric addressing tied to `backendNodeId` values 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.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) for storage, [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) for resolution logic, and [`src/locator-query.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/locator-query.ts) for 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.